diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md
index f4b6818..d7ae76a 100644
--- a/.planning/ROADMAP.md
+++ b/.planning/ROADMAP.md
@@ -98,11 +98,11 @@ Plans:
**Depends on**: Phase 7
**Research**: Unlikely (internal fixes and integration of existing data)
**Gap Closure**: Closes session bug, additional work sections, Sonstiges gaps from v1.0 audit
-**Plans**: 0/1
-**Status**: Not started
+**Plans**: 2 plans
Plans:
-- [ ] 08-01: Fix session bug, render additional work sections, add Sonstiges
+- [ ] 08-01-PLAN.md — Fix session_id() bug and validation error format inconsistency
+- [ ] 08-02-PLAN.md — Render additional work sections and Sonstiges in form, handler, and email
### Phase 9: Internationalization
**Goal**: Wrap all user-facing strings in gettext functions, create .pot/.po/.mo translation files, load text domain, provide German and English translations
@@ -126,5 +126,5 @@ Plans:
| 5. Volume Calculations | 1/1 | Complete | 2026-01-16 |
| 6. Email System | 1/1 | Complete | 2026-01-16 |
| 7. Captcha & Validation | 1/1 | Complete | 2026-01-16 |
-| 8. Bug Fixes & Legacy Parity | 0/1 | Not started | - |
+| 8. Bug Fixes & Legacy Parity | 0/2 | Planned | - |
| 9. Internationalization | 0/1 | Not started | - |
diff --git a/.planning/phases/08-bugfixes-legacy-parity/08-01-PLAN.md b/.planning/phases/08-bugfixes-legacy-parity/08-01-PLAN.md
new file mode 100644
index 0000000..24f8e98
--- /dev/null
+++ b/.planning/phases/08-bugfixes-legacy-parity/08-01-PLAN.md
@@ -0,0 +1,136 @@
+---
+phase: 08-bugfixes-legacy-parity
+plan: 01
+type: execute
+wave: 1
+depends_on: []
+files_modified:
+ - includes/class-form-renderer.php
+ - includes/class-form-handler.php
+autonomous: true
+
+must_haves:
+ truths:
+ - "Each form submission gets a unique transient key so error messages never leak between users"
+ - "Validation errors display correctly after redirect (captcha errors and field validation errors alike)"
+ - "Transient is deleted after errors are displayed so errors do not persist on refresh"
+ artifacts:
+ - path: "includes/class-form-renderer.php"
+ provides: "Hidden form_id field, error retrieval via form_id from GET param"
+ contains: "umzugsliste_form_id"
+ - path: "includes/class-form-handler.php"
+ provides: "Form ID extraction from POST, transient keyed by form_id, redirect with form_id param"
+ contains: "umzugsliste_form_id"
+ key_links:
+ - from: "includes/class-form-renderer.php"
+ to: "includes/class-form-handler.php"
+ via: "Hidden input umzugsliste_form_id posted to handler, handler stores transient with that key, redirects with form_id in query string, renderer reads form_id from GET and retrieves matching transient"
+ pattern: "umzugsliste_form_id"
+---
+
+
+Fix the session_id() bug that causes error message cross-contamination between users, and fix the validation error format inconsistency.
+
+Purpose: session_id() returns empty string on most WordPress hosts (no session_start() called), causing ALL form error transients to share the key "umzugsliste_errors_default". This means User A's captcha failure message can appear on User B's form. This is a production-blocking bug.
+
+Additionally, validate_submission() returns a flat array of strings but render_validation_errors() expects an array with a 'messages' key. Only captcha errors use the correct format, so non-captcha validation errors silently fail to display.
+
+Output: Patched class-form-renderer.php and class-form-handler.php with per-submission unique IDs replacing session_id().
+
+
+
+@~/.claude/get-shit-done/workflows/execute-plan.md
+@~/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/STATE.md
+@.planning/phases/08-bugfixes-legacy-parity/08-RESEARCH.md
+@includes/class-form-renderer.php
+@includes/class-form-handler.php
+
+
+
+
+
+ Task 1: Replace session_id() with hidden form ID in renderer and handler
+ includes/class-form-renderer.php, includes/class-form-handler.php
+
+ **In class-form-renderer.php:**
+
+ 1. In render_validation_errors() (currently lines 47-72), replace the session_id() lookup with form_id from GET parameter:
+ - Remove the session_id() call and 'default' fallback entirely
+ - Check for form_id in `$_GET['form_id']` (this is set by the handler redirect)
+ - Sanitize with `sanitize_text_field()`
+ - If empty form_id, return early (no errors to show)
+ - Use `get_transient( 'umzugsliste_errors_' . $form_id )` to retrieve errors
+ - After retrieving, `delete_transient()` immediately (existing pattern, keep it)
+ - The rest of the error display logic stays the same (it already checks `$errors['messages']`)
+
+ 2. In render_submit_section() (currently lines 351-369), add a hidden field for the form ID:
+ - Generate a unique ID: `$form_id = 'umzug_' . uniqid( '', true );`
+ - Add it as a hidden input AFTER the existing `umzugsliste_submit` hidden input:
+ ``
+
+ **In class-form-handler.php:**
+
+ 1. At the top of handle_submission() (after nonce verification, before captcha check), extract the form_id from POST:
+ ```php
+ $form_id = isset( $_POST['umzugsliste_form_id'] ) ? sanitize_text_field( $_POST['umzugsliste_form_id'] ) : '';
+ if ( empty( $form_id ) ) {
+ $form_id = 'umzug_' . uniqid( '', true );
+ }
+ ```
+
+ 2. Replace the captcha error transient (currently line 72):
+ - Change `session_id()` to `$form_id`
+ - The redirect on line 73 must append form_id: `wp_safe_redirect( add_query_arg( 'form_id', $form_id, wp_get_referer() ) );`
+
+ 3. Fix the validation error format AND replace session_id() (currently lines 80-85):
+ - Wrap $validation_errors in the expected format: `array( 'messages' => $validation_errors, 'fields' => array() )`
+ - Change `session_id()` to `$form_id`
+ - The redirect must also append form_id: `wp_safe_redirect( add_query_arg( 'form_id', $form_id, wp_get_referer() ) );`
+
+ **Important: Do NOT use session_id() anywhere.** Remove all references to it. The hidden field + GET parameter approach is the WordPress-native pattern that works on all hosting.
+
+
+ Run `php -l includes/class-form-renderer.php && php -l includes/class-form-handler.php` to confirm no syntax errors.
+ Run `grep -r "session_id" includes/` to confirm zero matches (session_id completely removed).
+ Run `grep -c "umzugsliste_form_id" includes/class-form-renderer.php includes/class-form-handler.php` to confirm the new form_id field appears in both files.
+ Run `grep "messages" includes/class-form-handler.php` to confirm validation errors now use the 'messages' key format.
+
+
+ - session_id() is completely removed from both files
+ - Hidden field `umzugsliste_form_id` is generated with uniqid() in the form
+ - Form handler extracts form_id from POST, uses it for transient keys
+ - Redirects append `form_id` as GET parameter
+ - Renderer reads form_id from GET parameter to retrieve correct transient
+ - Both captcha errors and validation errors use the same `array( 'messages' => ..., 'fields' => ... )` format
+ - Transients are deleted after display (no stale errors)
+
+
+
+
+
+
+1. `php -l includes/class-form-renderer.php` exits 0
+2. `php -l includes/class-form-handler.php` exits 0
+3. `grep -r "session_id" includes/` returns no matches
+4. `grep "umzugsliste_form_id" includes/class-form-renderer.php` returns matches for hidden input generation
+5. `grep "umzugsliste_form_id" includes/class-form-handler.php` returns matches for POST extraction
+6. `grep "'messages'" includes/class-form-handler.php` returns matches showing the wrapped format
+
+
+
+- Zero references to session_id() in the entire includes/ directory
+- form_id hidden field present in rendered form HTML
+- Error transients keyed by unique form_id, not shared 'default' key
+- Validation error format matches what render_validation_errors() expects (array with 'messages' key)
+- All redirects after error include form_id in query string
+
+
+
diff --git a/.planning/phases/08-bugfixes-legacy-parity/08-02-PLAN.md b/.planning/phases/08-bugfixes-legacy-parity/08-02-PLAN.md
new file mode 100644
index 0000000..b3d180b
--- /dev/null
+++ b/.planning/phases/08-bugfixes-legacy-parity/08-02-PLAN.md
@@ -0,0 +1,333 @@
+---
+phase: 08-bugfixes-legacy-parity
+plan: 02
+type: execute
+wave: 2
+depends_on: ["08-01"]
+files_modified:
+ - includes/class-form-renderer.php
+ - includes/class-form-handler.php
+ - includes/class-email-generator.php
+ - assets/css/form.css
+autonomous: true
+
+must_haves:
+ truths:
+ - "Form displays 6 additional work sections (Montage, Schrank, Elektriker, Duebelarbeiten, Packarbeiten, Anfahrt) between room tables and grand totals"
+ - "Each additional work section renders the correct field type: checkbox, abbau_aufbau radio, checkbox_anzahl, or text"
+ - "Form displays a Sonstiges free text textarea after additional work sections"
+ - "Submitted additional work data appears in the email in legacy HTML table format"
+ - "Submitted Sonstiges text appears in the email"
+ - "Additional work data and Sonstiges are sanitized and saved to CPT"
+ artifacts:
+ - path: "includes/class-form-renderer.php"
+ provides: "render_additional_work_sections(), render_additional_work_section(), render_sonstiges_field() methods"
+ contains: "render_additional_work_sections"
+ - path: "includes/class-form-handler.php"
+ provides: "Sanitization of additional_work and sonstiges POST data"
+ contains: "additional_work"
+ - path: "includes/class-email-generator.php"
+ provides: "generate_additional_work_sections(), generate_sonstiges_section() methods"
+ contains: "generate_additional_work_sections"
+ - path: "assets/css/form.css"
+ provides: "Styling for additional work section field types"
+ contains: "additional-work"
+ key_links:
+ - from: "includes/class-form-renderer.php"
+ to: "includes/class-furniture-data.php"
+ via: "Umzugsliste_Furniture_Data::get_additional_work()"
+ pattern: "get_additional_work"
+ - from: "includes/class-form-handler.php"
+ to: "includes/class-email-generator.php"
+ via: "Sanitized additional_work and sonstiges data passed to email generator"
+ pattern: "additional_work"
+ - from: "includes/class-email-generator.php"
+ to: "includes/class-furniture-data.php"
+ via: "get_additional_work() for section labels in email generation"
+ pattern: "get_additional_work"
+---
+
+
+Integrate the 6 additional work sections and Sonstiges free text field into the form, form handler, and email generator.
+
+Purpose: Phase 2 extracted 32 fields across 6 additional work sections (Montage, Schrank, Elektriker, Duebelarbeiten, Packarbeiten, Anfahrt) from the legacy form, but they were never wired into the form rendering, submission handling, or email generation. The Sonstiges free text field is also missing. These are required for full legacy feature parity.
+
+Output: Complete form rendering of all additional work sections with correct field types, sanitization in the handler, and legacy-format HTML table generation in the email for both additional work and Sonstiges.
+
+
+
+@~/.claude/get-shit-done/workflows/execute-plan.md
+@~/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/STATE.md
+@.planning/phases/08-bugfixes-legacy-parity/08-RESEARCH.md
+@.planning/phases/08-bugfixes-legacy-parity/08-01-SUMMARY.md
+@includes/class-form-renderer.php
+@includes/class-form-handler.php
+@includes/class-email-generator.php
+@includes/class-furniture-data.php
+@assets/css/form.css
+
+
+
+
+
+ Task 1: Render additional work sections and Sonstiges in the form
+ includes/class-form-renderer.php, assets/css/form.css
+
+ **In class-form-renderer.php:**
+
+ 1. In the render() method, add two new calls between `self::render_all_rooms()` and `self::render_grand_totals()`:
+ ```php
+ self::render_additional_work_sections();
+ self::render_sonstiges_field();
+ ```
+
+ 2. Add `render_additional_work_sections()` private static method:
+ - Call `Umzugsliste_Furniture_Data::get_additional_work()` to get section data
+ - Loop through each section, calling `self::render_additional_work_section( $section_key, $section_data )`
+
+ 3. Add `render_additional_work_section( $section_key, $section_data )` private static method:
+ - Render a panel header with `$section_data['label']` (same pattern as room sections: div.row > div.large-12.columns > div.panel > h3)
+ - Render a container div with class `additional-work-section` and `data-section="$section_key"`
+ - Loop through `$section_data['fields']` and render each field based on its 'type':
+
+ **Field type rendering (all fields use `name="additional_work[$section_key][$field_key]"` where $field_key is the field's 'key' if present, otherwise a sanitized version of the field 'name'):**
+
+ a) `checkbox` type:
+ - A single row with: label text + checkbox input (value="ja")
+ - Use `name="additional_work[{$section_key}][{$field_key}]"` with value="ja"
+ - Pattern: `
`
+
+ b) `abbau_aufbau` type:
+ - A row with: label text + three radio buttons (Abbau, Aufbau, Beides)
+ - Use `name="additional_work[{$section_key}][{$field_key}]"` with values "Abbau", "Aufbau", "Beides"
+ - Pattern: `
`
+
+ c) `checkbox_anzahl` type:
+ - A row with: checkbox + label text + small text input for quantity
+ - Use checkbox `name="additional_work[{$section_key}][{$field_key}]"` value="ja" and text input `name="additional_work[{$section_key}][{$field_key}_anzahl]"`
+ - Pattern: `
`
+
+ d) `text` type:
+ - A row with: label text + text input
+ - Use `name="additional_work[{$section_key}][{$field_key}]"`
+ - Pattern: `
`
+
+ **For generating $field_key:** Use the field's 'key' property if it exists (some anfahrt fields have explicit keys like 'LKWBeladestelle'). Otherwise, sanitize the field name: `sanitize_title( $field['name'] )` to create a URL-safe key. Store this logic in a small private helper `get_field_key( $field )`.
+
+ 4. Add `render_sonstiges_field()` private static method:
+ - Render a panel header "Sonstiges" (same panel pattern as above)
+ - Render a textarea: ``
+ - Wrap in the standard row/columns layout
+
+ 5. Add helper method `get_field_key( $field )`:
+ - If `$field['key']` is set and not empty, return it
+ - Otherwise return `sanitize_title( $field['name'] )`
+
+ **In assets/css/form.css:**
+
+ Add styles at the end of the file (before the closing comment if any):
+
+ ```css
+ /* Additional Work Sections */
+ .umzugsliste-wrapper .additional-work-section {
+ margin-bottom: 1.25rem;
+ padding: 0 0.9375rem;
+ }
+
+ .umzugsliste-wrapper .additional-work-section .row {
+ margin-bottom: 0.5rem;
+ align-items: center;
+ }
+
+ .umzugsliste-wrapper .additional-work-section input[type="checkbox"] {
+ margin-right: 0.5rem;
+ }
+
+ .umzugsliste-wrapper .additional-work-section input[type="text"] {
+ margin-bottom: 0;
+ height: 2rem;
+ }
+
+ .umzugsliste-wrapper .additional-work-section label {
+ display: inline;
+ margin-bottom: 0;
+ }
+
+ /* Sonstiges */
+ .umzugsliste-wrapper .sonstiges-textarea {
+ width: 100%;
+ padding: 0.5rem;
+ border: 1px solid #ccc;
+ font-size: 0.875rem;
+ margin-bottom: 1rem;
+ resize: vertical;
+ min-height: 100px;
+ }
+ ```
+
+
+ Run `php -l includes/class-form-renderer.php` to confirm no syntax errors.
+ Run `grep -c "render_additional_work_sections\|render_sonstiges_field\|get_field_key" includes/class-form-renderer.php` to confirm all 3 new methods exist.
+ Run `grep "get_additional_work" includes/class-form-renderer.php` to confirm the data source is wired.
+ Run `grep "additional-work" assets/css/form.css` to confirm CSS is added.
+
+
+ - 6 additional work sections render in the form between rooms and grand totals
+ - Each field type (checkbox, abbau_aufbau, checkbox_anzahl, text) renders correctly with proper field names
+ - Sonstiges textarea renders after additional work sections
+ - CSS styling keeps sections visually consistent with existing form
+ - All field names follow `additional_work[$section][$key]` pattern for clean POST data structure
+
+
+
+
+ Task 2: Sanitize additional work data and generate email sections
+ includes/class-form-handler.php, includes/class-email-generator.php
+
+ **In class-form-handler.php:**
+
+ 1. In sanitize_submission() (after the room arrays sanitization block, around line 230), add sanitization for additional_work:
+ ```php
+ // Sanitize additional work sections
+ if ( ! empty( $data['additional_work'] ) && is_array( $data['additional_work'] ) ) {
+ $sanitized['additional_work'] = array();
+ foreach ( $data['additional_work'] as $section_key => $section_data ) {
+ if ( is_array( $section_data ) ) {
+ $sanitized['additional_work'][ sanitize_key( $section_key ) ] = array();
+ foreach ( $section_data as $field_key => $value ) {
+ $sanitized['additional_work'][ sanitize_key( $section_key ) ][ sanitize_key( $field_key ) ] = sanitize_text_field( $value );
+ }
+ }
+ }
+ }
+
+ // Sanitize Sonstiges
+ if ( ! empty( $data['sonstiges'] ) ) {
+ $sanitized['sonstiges'] = sanitize_textarea_field( $data['sonstiges'] );
+ }
+ ```
+ Note: Use `sanitize_key()` for array keys (lowercase, alphanumeric, dashes, underscores) and `sanitize_text_field()` for values. Use `sanitize_textarea_field()` for Sonstiges (preserves newlines).
+
+ **In class-email-generator.php:**
+
+ 1. In the generate() method (after `$content .= self::generate_all_rooms( $data );` and before `$content .= self::generate_grand_totals( $data );`), add:
+ ```php
+ // Additional work sections
+ $content .= self::generate_additional_work_sections( $data );
+
+ // Sonstiges
+ if ( ! empty( $data['sonstiges'] ) ) {
+ $content .= self::generate_sonstiges_section( $data['sonstiges'] );
+ }
+ ```
+
+ 2. Add `generate_additional_work_sections( $data )` private static method:
+ - Get sections from `Umzugsliste_Furniture_Data::get_additional_work()`
+ - For each section, check if there is any data in `$data['additional_work'][$section_key]`
+ - Use helper `has_additional_work_data( $data, $section_key )` to check -- returns true if any value in the section is non-empty
+ - If has data, generate a table for that section:
+
+ ```html
+
+
+
+
+
+
{section_label}
+
+
+
+ ```
+
+ For each field in the section that has data:
+ - `checkbox`: If value is "ja", show row with field name and "Ja"
+ - `abbau_aufbau`: If value is set (Abbau/Aufbau/Beides), show row with field name and the value
+ - `checkbox_anzahl`: If checkbox is "ja", show row with field name and optionally " (Anzahl: X)" if _anzahl value exists
+ - `text`: If value is non-empty, show row with field name and value
+
+ Each row pattern: `
{field_name}
{value}
`
+
+ Close the table: `
`
+
+ Only include sections that have at least one field with data (skip entirely empty sections to keep email clean, matching the room pattern of omitting empty rooms).
+
+ 3. Add `has_additional_work_data( $data, $section_key )` private static method:
+ - Check if `$data['additional_work'][$section_key]` exists and is a non-empty array
+ - Return true if any value in that array is non-empty after trimming
+ - Return false otherwise
+
+ 4. Add `generate_sonstiges_section( $sonstiges_text )` private static method:
+ - Generate a simple table section:
+ ```html
+
+
+
+
+
+
Sonstiges
+
+
+
+
+
{escaped sonstiges text with nl2br for line breaks}
+
+
+
+
+
+ ```
+ Use `nl2br( esc_html( $sonstiges_text ) )` to preserve line breaks in the email.
+
+ **Important:** The email table structure must use the same bgcolor='#CCCCCC' header pattern as existing room sections. The legacy office staff expect consistent formatting.
+
+
+ Run `php -l includes/class-form-handler.php && php -l includes/class-email-generator.php` to confirm no syntax errors.
+ Run `grep -c "additional_work\|sonstiges" includes/class-form-handler.php` to confirm sanitization code was added.
+ Run `grep -c "generate_additional_work_sections\|generate_sonstiges_section\|has_additional_work_data" includes/class-email-generator.php` to confirm all 3 new methods exist.
+ Run `grep "get_additional_work" includes/class-email-generator.php` to confirm data source wired.
+ Run `grep "sanitize_textarea_field" includes/class-form-handler.php` to confirm Sonstiges uses textarea-appropriate sanitization.
+
+
+ - additional_work POST data is sanitized (keys with sanitize_key, values with sanitize_text_field)
+ - Sonstiges text is sanitized with sanitize_textarea_field (preserves newlines)
+ - Email generator produces legacy-format HTML tables for each additional work section that has data
+ - Empty additional work sections are omitted from email (clean format)
+ - Sonstiges appears in email with line breaks preserved
+ - All field types (checkbox, abbau_aufbau, checkbox_anzahl, text) render correctly in email
+ - Both additional work and Sonstiges data are included in CPT JSON storage (via sanitize_submission passing them through)
+
+
+
+
+
+
+1. `php -l includes/class-form-renderer.php` exits 0
+2. `php -l includes/class-form-handler.php` exits 0
+3. `php -l includes/class-email-generator.php` exits 0
+4. `grep "get_additional_work" includes/class-form-renderer.php includes/class-email-generator.php` returns matches in both files
+5. `grep "sonstiges" includes/class-form-renderer.php includes/class-form-handler.php includes/class-email-generator.php` returns matches in all 3 files
+6. `grep "additional_work" includes/class-form-handler.php` returns matches for sanitization
+7. `grep "additional-work" assets/css/form.css` returns matches for styling
+8. All field types accounted for: `grep -c "checkbox\|abbau_aufbau\|checkbox_anzahl" includes/class-form-renderer.php` returns 4+ matches
+
+
+
+- 6 additional work sections render in the form with correct field types
+- Sonstiges textarea renders in the form
+- All POST data is sanitized appropriately (text fields, textarea, keys)
+- Email includes additional work sections with data in legacy table format
+- Email includes Sonstiges with preserved line breaks
+- Empty sections omitted from email
+- All data persists to CPT via JSON encoding in post_content
+- No PHP syntax errors in any modified file
+
+
+