Commit Graph
49 Commits
Author SHA1 Message Date
FrankandClaude Sonnet 5 d4d8cae00f Add app:character:seed so party sheets can reach every environment
Darragh Oakwanderer never made it past the local database - the
Characters migration only creates the empty table, and I'd added him
with a one-off script that was never committed. This closes that gap
the same way app:enemy:seed does: a small catalog of ready-made sheets,
picked interactively or by name, idempotent (skips what's already
there), with --purge to remove them.

Verified against the local DB: purge, add, and a repeat add (correctly
reports 0 added / 1 already present) all produce exactly the row I'd
checked by hand earlier.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 09:02:00 +02:00
FrankandClaude Sonnet 5 9052218927 Characters: build the page around Darragh Oakwanderer's sheet
Replaces the Characters placeholder with a real 5e character sheet
feature, following the same read-only-detail/edit-split/game-notes
pattern as NPCs and Enemies:

- Character entity: identity, combat vitals, six ability scores, saving
  throw and skill proficiencies (computed modifiers, not stored), a
  small attacks table, and the sheet's freeform boxes (features,
  equipment, personality, appearance, backstory, allies, treasure).
- Detail page shows the portrait uncropped, then the sheet - genuine
  <table>s for ability scores, skills and attacks, since that's the one
  part of a character sheet that actually is a table; everything else
  reuses the prose helpers from the enemy stat block. Game notes are
  the only editable field here, same as Enemy/NPC.
- Edit page covers every field via CharacterType; the attack rows are
  six plain inputs handled by hand (not a Symfony collection field),
  since it's a short fixed-shape table, not a dynamic list.
- Portraits follow the NPC convention: assets/image/characters/<NameNo
  Spaces>.png, computed path, not a DB column.
- Imported the example sheet from assets/characters/ (Darragh
  Oakwanderer, halfling ranger) directly into the database.

Tests: entity math (ability/save/skill modifiers, passive perception,
portrait path), template rendering for show/edit/index (catches broken
Twig/macro/path()/asset() calls), and the login-gate redirect. 21/21
passing; schema validate clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 08:55:28 +02:00
FrankandClaude Sonnet 5 bdf676f1dd Add villager Clover Thorn (18), beekeeper Thorn family, with portrait
Halfling daughter of Bramble and Poppy Thorn, younger sister of Wren.
Portrait cropped of its frame and downsized to 450px wide.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 23:29:59 +02:00
FrankandClaude Sonnet 5 b77e5eaa4f Storyline: unfoldable section list per chapter in the sidebar
Each chapter in the right-hand menu gets a caret that unfolds its
numbered sections (e.g. the prologue's 28 scenes); clicking one jumps
straight to it. The chapter you're reading starts unfolded, other
chapters' sections link across (/storyline/overview#section-…), and the
section at the top of the pane is highlighted as you scroll.

- ChapterOutline service parses each rendered chapter for its
  <h2 class="storyline-section"> headings, adds anchor ids and returns the
  outline; the controller renders all chapters once to feed the sidebar.
- storyline-toc Stimulus controller: fold/unfold, in-pane jump, scroll-spy
  (works in the split view and in the small-screen window-scroll fallback).
- Tests for the outline parser.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 18:44:54 +02:00
FrankandClaude Sonnet 5 8b8f67c1d0 Art: drag pieces into a new order
Each gallery tile gets a small drag handle (top-left, ✱). Grab it and
drop it anywhere among the others - the tile moves live as you drag
over neighbours, then the new order is saved automatically on drop.

- Artwork gets a `position` column; the gallery lists by position (was
  upload date). New uploads land after everything else.
- New POST /art/reorder endpoint takes an ordered list of ids (one
  shared CSRF token, "reorder-art") and rewrites positions 1..n.
- Migration backfills position from the existing upload-date order via
  ROW_NUMBER(), then drops the bootstrap default so it matches the
  Doctrine mapping.
- New Stimulus controller (art-reorder): native HTML5 drag/drop, no
  library. dragover live-reorders the DOM by comparing cursor x to the
  hovered tile's midpoint; dragend posts the resulting id order and
  shows a small "Saving order… / Order saved" status pill.

Verified: schema validate clean: a throwaway script exercised
persist+reload of a reordered set against the real DB, and a live
browser check dispatched real DragEvents through the actual Stimulus
controller and confirmed the exact POST payload it sent. phpunit 9/9.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-15 17:18:02 +02:00
FrankandClaude Sonnet 5 bc0433e356 Fix puzzle-relay exiting immediately on Linux: force start command
Worker::runAll() parses start/stop/restart/... off the real \$argv on
Linux - which here is Symfony console's own arguments (bin/console
app:puzzle-relay), not a command it recognizes, so it printed its
usage text and exited on every boot, and Docker kept restarting it
forever. Windows skips that argv parsing entirely, which is why this
never showed up locally. Worker::\$command = 'start' is Workerman's
own mechanism for exactly this "used as a library" case - it appends
to what getArgv() returns without touching the real \$argv.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-12 13:57:00 +02:00
FrankandClaude Sonnet 5 9ba4197bfd Fix puzzle-relay crash loop: install pcntl/posix extensions
Workerman requires pcntl and posix on Linux; without them the relay
printed "Please install pcntl extension" and exited immediately,
Docker kept restarting it, and nginx's DNS lookups for
dmtools-puzzle-relay intermittently failed whenever it happened to be
down mid-restart - the "could not be resolved" errors were a symptom
of this crash loop, not a separate networking problem. Local dev never
hit this because Workerman's Windows code path doesn't need either
extension.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-12 13:49:07 +02:00
FrankandClaude Sonnet 5 de908d547f Fix .env.prod never reaching the puzzle relay containers
docker-compose's env_file loads .env directly into each container's
real environment - and Symfony's Dotenv never lets a .env.$APP_ENV
file override a variable the platform already set. Since .env.prod
was never in the env_file list, PUZZLE_RELAY_PUBLIC_URL kept resolving
to the dev default (ws://127.0.0.1:8091) in production instead of the
wss://dmtools.fvandenberg.nl/puzzle-ws/ override. Load .env.prod here
too, after .env/.env.local, so its values actually win.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-12 13:43:04 +02:00
FrankandClaude Sonnet 5 efba34a1e2 Deploy the puzzle relay alongside the container stack
Adds a puzzle-relay service (docker-compose.yml) running the same
image as php but executing app:puzzle-relay instead of php-fpm, kept
off the host network - only nginx can reach it. nginx proxies
wss://dmtools.fvandenberg.nl/puzzle-ws/ through to it
(docker/nginx/default.conf), with the public URL set via a new
.env.prod. setup.sh/restart.sh now rebuild, restart, and clean up the
new container alongside the existing ones.

Still needs "Websockets Support" enabled on the NPM proxy host for
this domain, or the upgrade headers never reach the container.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-12 13:36:38 +02:00
FrankandClaude Sonnet 5 2ccbf4ca67 Add shared multiplayer gear puzzle (Chamber of Motion)
Players open a no-login link and see live cursors and dragged gear
pieces synced in real time, backed by a small Workerman WebSocket
relay (src/Command/PuzzleRelayCommand.php) run alongside symfony
serve - no Docker/Mercure hub needed. The board renders the wall
artwork with a spare-gears tray, and a "Reset puzzle" button (visible
only to logged-in users, verified server-side via a signed token) lets
the DM wipe the shared state for everyone.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-12 13:24:42 +02:00
FrankandClaude Sonnet 5 40dc30f950 Storyline: hide the chapter-pane scrollbar (scrolling still works)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 19:02:23 +02:00
FrankandClaude Sonnet 5 90345ecd38 Storyline: split the chapter page, dock the notes at the bottom
The chapter page now fills the viewport as two horizontal panes: the
chapter scrolls in the top ~2/3, the notes sit in a fixed dock in the
bottom ~1/3, always visible while you read. Drag the divider to change
the ratio, click the "Chapter notes" header to collapse the dock to a
bar; both the height and the collapsed state persist in localStorage.

- base.html.twig: add a {% block body_class %} hook on <body>.
- chapter.html.twig sets body class "storyline-chapter-page"; the split
  layout CSS is scoped to it and falls back to normal stacked flow under
  768px wide or 560px tall.
- app.js publishes the navbar height as --app-nav-h so the split can size
  itself to viewport-minus-navbar even when the navbar wraps.
- new chapter_notes Stimulus controller: drag-resize + collapse + persist.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 19:00:49 +02:00
FrankandClaude Sonnet 5 5c821bd7ad Storyline: per-chapter DM notes
Each chapter page gets a collapsible "Chapter notes" card at the top — a
free-text log the DM fills during play (the story the party tells after
the Crooked Goose, names given to NPCs, threads for next time). Saved
per chapter slug with a "saved <timestamp>" marker, same shape as the
Enemy/NPC game-notes field.

New: ChapterNote entity (slug-keyed, unique) + repository findOrCreate(),
ChapterNoteType form, chapter_note table migration (ENGINE = InnoDB
pinned). The storyline chapter route now also accepts POST to save.

Chapters stay hand-written Twig; only the mutable notes live in the DB.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 18:51:53 +02:00
FrankandClaude Sonnet 5 56f4641564 Storyline: add the Prologue chapter, extend the layout for sessions
Chapters now carry an eyebrow ("Overview", "Prologue", ...) and a short
tag for the list/sidebar badge, instead of a bare running number, so
non-numbered entries read correctly.

New chapter: "Prologue - The Long Road to Brackenford", the session
outline formatted as a runnable document -- an at-a-glance metadata band
(levels, duration, locations, hook), the in-world courier notice as a
posted handout, numbered scenes, GM callouts for the "ask the players"
prompts, label/body rows for the skill-check clues and travel-day
prompts, a three-stage encounter block, dramatic beat lines, and a
before/after panel for the legend the party's own retelling becomes.
Cross-links the Hearthburrow to Enemies and the closing line to the
overview's "Five and the Five".

Tests: render every chapter partial headless (no HTTP/DB) so a broken
variable or path() fails loudly, check each registered chapter has its
template file, and assert the /storyline routes sit behind the login gate.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 18:19:07 +02:00
FrankandClaude Sonnet 5 7fbf6f1569 Storyline: chapter structure + formatted Campaign Overview
The Storyline section is now a set of hand-written chapters. /storyline
lists them; /storyline/{slug} renders one inside a shell with a sticky
chapter nav and prev/next pager. Chapters are declared in
StorylineController::CHAPTERS, each pointing at a Twig partial under
templates/storyline/chapters/.

First chapter is the Brackenford Campaign Overview: the campaign-bible
text laid out as a long-form reading page -- numbered section headers,
a "by the numbers" / "the cast" quick-reference strip, pull-quotes for
in-world lines, GM callouts for the ticking clocks and the red herring,
and the escalation ladder. Storyline styles added to app.css.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 17:28:21 +02:00
FrankandClaude Sonnet 5 373fa6f78b enemy:seed: pick creatures from a list, add Hearthburrow
The command now prompts with a numbered multi-select list instead of
seeding all monsters unconditionally. Names can also be passed as
arguments (case-insensitive) and --all keeps the old bulk behaviour for
non-interactive runs. Adds the homebrew Hearthburrow (Tiny Fey, CR 1/2)
to the catalogue, with its Cottage Actions in the legendary-actions
field and the Temper track in the description.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-07 22:59:03 +02:00
FrankandClaude Sonnet 5 8d9d7874f3 setup.sh: restart php after cache warm
opcache.validate_timestamps=0 means php-fpm won't see updated
code/templates until it restarts. Add an explicit `dc restart php` at the
end so `./docker/setup.sh --no-build` reliably deploys a git pull.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 21:44:43 +02:00
FrankandClaude Sonnet 5 bc42adcad9 Add delete button on the enemy detail page
CSRF-protected POST to /enemies/{id}/delete with a confirm. Any
combatants referencing the enemy keep their snapshotted stats (the FK
sets enemy_id null), so deleting a monster type won't disturb a fight.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 21:39:12 +02:00
FrankandClaude Sonnet 5 684cbf3b4f docs: README reverse-proxy section matches the NPM network reality
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 21:32:30 +02:00
FrankandClaude Sonnet 5 8406f06738 docker: NPM joins dmtools_default, not a shared proxy network
NPM (nginx_app_1) is connected to each stack's own default network by
hand, not to a shared docker_default. Drop the unused external `proxy`
network from the compose file; nginx just lives on dmtools_default.
setup.sh now connects NPM to it (no-op if already connected) and prints
the proxy-host settings.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 21:32:10 +02:00
FrankandClaude Sonnet 5 25b68cb1df Fix db host-port collision: separate DB_HOST_PORT from the internal port
DB_PORT was doing double duty - the container-internal port baked into
DATABASE_URL (must be 3306) and the host-published port in
docker-compose.yml. Setting it to 3306 for the URL made compose try to
publish host :3306, which collides with another stack's MySQL.

DATABASE_URL now hardcodes database:3306 (always correct inside the
compose network); the host publish uses ${DB_HOST_PORT:-3310}.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 21:07:51 +02:00
FrankandClaude Sonnet 5 ade11494be docker/php: build extensions via mlocati/php-extension-installer
Hand-managed apk build deps + docker-php-ext-install was failing to build
on the server. Switch to the extension-installer image (same approach the
escapepage stack uses), which resolves system libs itself.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 20:59:57 +02:00
FrankandClaude Sonnet 5 7a9f72a3de Overview page: navigation tiles instead of the demo table
Replaces the leftover scaffold DataTable with a grid of tiles linking to
Characters, NPC's, Enemies, Tracker, Art and Storyline.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 19:43:37 +02:00
FrankandClaude Sonnet 5 cb23d5b9ac Show AC on each tracker row
Combatant snapshots armor_class from the enemy when added (like maxHp),
shown as an "AC n" pill next to the name for quick hit checks. Migration
backfills existing combatants from their enemy.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 19:34:24 +02:00
FrankandClaude Sonnet 5 2ed169fc78 Tracker: unfold a combatant's stat block inline
- extract the 5e stat-block markup into enemy/_statblock.html.twig
  (sb.block(enemy) macro); the enemy detail page now uses it too
- each tracker combatant gets a "stat block" toggle that expands the
  block in place (Bootstrap collapse), so you don't have to leave the
  encounter to check AC / attacks
- open blocks are remembered in localStorage and re-opened after each
  damage/heal reload, and the caret rotates when expanded

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 19:29:52 +02:00
FrankandClaude Sonnet 5 cbe4b0a0e1 Combat tracker: multiple named encounters
- Encounter entity; Combatant now belongs to an encounter (ON DELETE
  CASCADE). Migration adds encounter_id, rehoming any existing combatants.
- /tracker lists encounters (name, count, last updated) with a "New
  encounter" form; /tracker/{id} is the HP tracker for one encounter.
- Encounters can be renamed, have their combatants cleared, or be deleted.
- The Enemies list gains an encounter picker next to "Add to tracker"
  (existing encounter or "— New encounter —", which prompts for a name);
  an encounter's "Add enemies" link pre-selects it via ?encounter=.
- Routes for combatant HP/remove moved under /tracker/combatant/ to keep
  the numeric {id} for encounters.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 19:24:37 +02:00
FrankandClaude Sonnet 5 9854e6c034 Add combat HP tracker
- one running tracker (Combatant rows); no encounters to manage
- Enemies list gets a "Track" column (checkbox + count) and an
  "Add to tracker" button; picking N of an enemy makes "Goblin 1..N",
  numbering continues if some are already on the tracker
- /tracker: per-combatant current/max HP with an HP bar, Damage / Heal
  by amount, remove one, "Clear all". HP is clamped to [0, max];
  0 HP shows a "Down" badge. Stats are snapshotted from the Enemy on add.
- "Tracker" nav item; tracker_picker Stimulus controller; app.js gains a
  data-format="pick" cell renderer

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 19:16:12 +02:00
FrankandClaude Sonnet 5 e417ff9655 Add app:enemy:seed - 8 SRD monsters for testing the Enemies section
Goblin, Wolf, Zombie, Bandit Captain, Ogre, Owlbear, Wight and Adult Red
Dragon (CR 1/4 to 17; humanoid, beast, undead, giant, monstrosity,
dragon). Idempotent by name; --purge removes them.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 19:04:43 +02:00
FrankandClaude Sonnet 5 ed22b81e97 Art: full-image thumbnails, detail page with copy-to-clipboard
- gallery thumbnails now object-fit: contain (whole image, letterboxed),
  and a card links to the new detail page instead of the raw file
- /art/{id} detail page: large uncropped image, "Open full size",
  "Delete" (moved here from the gallery), and "Copy image"
- clipboard Stimulus controller copies the image to the OS clipboard
  (converts non-PNG via canvas) for pasting into WhatsApp etc.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 16:53:49 +02:00
FrankandClaude Sonnet 5 f04c8ccc51 Raise upload limit to 8 MB
- docker/php/php.ini: upload_max_filesize 8M / post_max_size 10M
- docker/nginx/default.conf: client_max_body_size 10M
- ArtworkType: Image maxSize 8M with a clear message + help text

(local C:\php\php.ini was bumped from 2M the same way — not in the repo)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 16:44:30 +02:00
FrankandClaude Sonnet 5 ed9390319b Add Art section: upload named images, gallery, delete
- Artwork entity (name, stored filename, original name, mime, size, date)
- /art gallery grid; /art/upload (name + image file, PNG/JPEG/GIF/WebP,
  20M cap); /art/{id}/file streams the image behind auth; /art/{id}/delete
  (CSRF) removes row + file
- files stored under var/uploads/art/ (git-ignored, on the php_var volume
  in Docker), served through a controller so they stay login-gated
- "Art" nav item

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 16:41:33 +02:00
FrankandClaude Sonnet 5 1f4b5290b2 Drop HP and Updated columns from the enemies table
Still on the entity and shown on the detail page.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 16:41:33 +02:00
FrankandClaude Sonnet 5 7870be9dfb Add Enemies section: D&D stat-block table + detail/edit pages
- Enemy entity: full 5e stat block (size/type/subtype/alignment, AC, HP,
  speed, six ability scores, CR/XP/prof, saves/skills/senses/languages,
  damage & condition modifiers, traits/actions/bonus/reactions/legendary,
  description, source) plus a DM game-notes field and updated_at.
- CreatureSize / CreatureType enums.
- /enemies: server-side DataTable (name, size, type, CR, AC, HP, notes),
  "Add enemy" button.
- /enemies/{id}: stat block rendered read-only + editable game notes.
- /enemies/new and /enemies/{id}/edit: the full stat-block form (no notes).
- "Enemies" nav item between NPC's and Storyline.
- Table left empty on purpose.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 16:36:17 +02:00
FrankandClaude Sonnet 5 574d903715 Add family tree page, linked from the NPC's family name
- /npcs/family/{family} (app_npc_family) renders a parent->child tree for
  everyone sharing that family name; co-parents who are both in the family
  are paired into one couple node, elders first
- FamilyTreeBuilder does the flat-list -> tree assembly
- the NPC detail page's family name (subtitle + Details row) now links there
- .family-tree CSS: nested list with connector lines, small portrait avatars

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 16:23:03 +02:00
FrankandClaude Sonnet 5 d43746970b Split NPC page: read-only detail + game notes, separate full edit form
- /npcs/{id} (app_npc_show): portrait, all fields shown read-only, and
  the only editable control is the game-notes textarea. Father/Mother
  link through to their own pages.
- /npcs/{id}/edit (app_npc_edit): the full structured form, now WITHOUT
  game notes; saving returns to the detail page.
- NpcType drops gameNotes; new NpcGameNotesType holds just that field.
- The DataTable row link (/npcs/{id}) now lands on the detail page.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 16:17:03 +02:00
FrankandClaude Sonnet 5 71b5cb6633 Show the NPC portrait on the detail page
Npc::getPortraitPath() maps the name to image/npc-pictures/<NameNoSpaces>.png;
the edit page renders it beside the heading (with family · occupation), and
drops the <img> via onerror when a portrait is missing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 16:10:00 +02:00
FrankandClaude Sonnet 5 4d5c3253ed Add final 41 NPC portraits; every NPC now has a picture
- 41 cut from the "Wren Thorn … Mira Rook" sheet (space-free names)
- removed 17 stale files that matched no NPC: the 16 hallucinated names
  from the first sheet (CalenCooper, *Mill, *Reed) now superseded by the
  real families, plus HiyHearth

assets/image/npc-pictures/ = 180 files, one per NPC.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 16:05:10 +02:00
FrankandClaude Sonnet 5 60a6904b22 Update portraits-needed list to the final 41 NPCs
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 15:53:13 +02:00
FrankandClaude Sonnet 5 1d043c35c8 Add 101 NPC portraits (lists 1 & 2), drop spaces from all filenames
- 50 portraits cut from the "Sten Voss … Ralf Brewster" sheet
- 51 from the "Mara Brewster … Poppy Thorn" sheet (incl. one stray
  "HiyHearth" the generator invented between Eli and May Hearth)
- existing 55 files renamed to remove spaces (Garrick Voss.png ->
  GarrickVoss.png)

assets/image/npc-pictures/ now holds 156 files.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 15:52:30 +02:00
FrankandClaude Sonnet 5 710b383bc1 Strip parent mentions from NPC relationships text
Parents now live in the father/mother fields, so "son/daughter/child of
X [and Y]" is redundant noise in Relationships / Notes.

- app:npc:strip-parent-text removes that clause and tidies leftover
  punctuation ("Son of X, husband of Y" -> "Husband of Y"; standalone
  "Son of X and Y" -> cleared). Keeps "grandson of", "father of",
  "daughter-in-law of". 92 rows cleaned.
- data/npcs.csv regenerated from the DB with the cleaned text plus new
  Father / Mother name columns
- app:npc:import resolves those optional columns in a second pass, so a
  fresh install reproduces the parent links without app:npc:link-parents

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 15:41:02 +02:00
FrankandClaude Sonnet 5 54d326572e Add 55 NPC portrait images (assets/image/npc-pictures/)
Cut from the contact sheet, one PNG per character named by the label
under each portrait. 39 match an existing NPC row; 16 (the extra Cooper
kids, the "Mill" family, the "Reed" block) have no matching NPC in the
database yet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 15:32:06 +02:00
FrankandClaude Sonnet 5 5bf25d1802 Add father/mother links to NPCs, derived from the relationships text
- Npc gains self-referencing father/mother (nullable, ON DELETE SET NULL);
  editable as dropdowns on the per-NPC form
- app:npc:link-parents parses "son/daughter/child of X [and Y]" from the
  Relationships / Notes column, resolves the names within the same family,
  and works out which parent is father vs mother from husband/wife/widow(er)
  and son/daughter/sister/brother cues (plus a small name-gender table for
  the few the text can't disambiguate). --dry-run / --force. 92/180 linked,
  none ambiguous.
- migrations now pin ENGINE=InnoDB - the local WAMP MariaDB defaults to
  MyISAM, which silently drops foreign keys

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 15:21:03 +02:00
FrankandClaude Sonnet 5 3c622d7dd8 Destroy DataTables before Turbo caches the page
The back button restored a Turbo snapshot that still held DataTables'
injected search / length controls; re-init on turbo:load then stacked
another set each time. Tear every instance down on turbo:before-cache
so the cached snapshot is a plain table again.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 15:11:59 +02:00
FrankandClaude Sonnet 5 01d5fd13b2 Drop Family, Race and Updated columns from the NPC table view
Still stored on the entity and editable on the per-NPC form; just no
longer shown in the /npcs list.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 15:07:36 +02:00
FrankandClaude Sonnet 5 02e3fe3eb6 Default DataTable page length to 10
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 15:06:32 +02:00
FrankandClaude Sonnet 5 1ccab6ed68 Add NPC database table with server-side DataTable and per-NPC game notes
- Npc entity (village spreadsheet columns) + game_notes text field for
  the DM's live play notes, plus updated_at touched on every change
- Race / VillageStatus / BloodCartInvolvement / MysteryImportance as
  string-backed PHP enums, rendered as dropdowns on the edit form
- /npcs: server-side DataTables (paging, global search and column sort
  all run in the backend via NpcRepository::findForDataTable); name cell
  links to the edit page, game notes shown truncated
- /npcs/{id}: Bootstrap-themed edit form (all fields + a large game
  notes textarea); Symfony forms now use the bootstrap_5 theme
- app.js: generic server-side DataTable wiring driven by data-* on the
  <table>/<th> (data-datatable-src, data-row-url, data-format)
- app:npc:import console command - upserts from data/npcs.csv by
  (family, name), never overwrites game_notes; --purge / --dry-run
- data/npcs.csv: 180 villagers exported from the working spreadsheet

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 15:04:39 +02:00
FrankandClaude Sonnet 5 cc4fbf425d Add nav menu with Characters, NPC's and Storyline pages
Navbar (collapsible) with active-state highlighting driven by a nav list
in base.html.twig. Three routed, auth-gated controllers each rendering a
shared placeholder partial - data models still to be designed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 14:36:17 +02:00
FrankandClaude Sonnet 5 9f6763040e Scaffold Symfony 8 app with Bootstrap/DataTables and Docker stack
- Symfony 8.1 webapp skeleton on PHP 8.5 (AssetMapper/importmap, no Node)
- Form-login gate: User entity, security.yaml, SecurityController,
  app:user:create command, initial migration (user + messenger_messages)
- Bootstrap 5 + DataTables layout (base/login/home templates); any
  table.datatable is auto-initialised
- trusted_proxies/headers wired for the shared Nginx Proxy Manager
- Docker (servers only): docker-compose.yml (php:8.5-fpm-alpine,
  nginx:1.30-alpine, mariadb:12.3; host ports 8085/3310; external
  docker_default network) + docker/{Dockerfile,php.ini,nginx,setup.sh,restart.sh}
- .env with CHANGEME placeholders (no secrets), .gitattributes enforces
  LF so the deploy scripts stay runnable on Linux

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 14:11:36 +02:00
Frank 39240a98ec Initial commit 2026-09-06 11:20:46 +00:00