Property Management Solutions
← All docs

Troubleshooting

╔══════════════════════════════════════════════════════════════════════════════╗
║                         TROUBLESHOOTING GUIDE                                ║
╚══════════════════════════════════════════════════════════════════════════════╝

Generated : 2026-07-16 (AR reverse TSV fix, ~80-col aging/exceptions, 1.20, lease onboard)

DATABASE ISSUES
═══════════════

  "Access denied for user 'pmuser'"
  → Ensure MariaDB is running:
      Linux:  systemctl status mariadb
      macOS:  brew services list   (or brew services start mariadb)
  → Verify user exists:         mariadb -u pmuser -D pmdb -e "SELECT 1"

  "Can't connect to local server"
  → Start MariaDB, then retry the menu option.
  → macOS Homebrew often uses socket /tmp/mysql.sock (see ~/.my.cnf)

  Schema looks wrong in Wiki
  → Press [do] Refresh Documentation from the main menu.
  → Or run:  cd scripts && ./update_docs.sh   # SAFE default = schema only
  → Never use --overwrite-curated unless you mean to destroy curated stubs
  → Policy: docs/wiki-CURATED.txt

QA LAB / SANDBOX
════════════════

  "QA guard: refused"
  → export PM_QA_MODE=1 PMS_DB_NAME=pmdb_qa

  Bootstrap: Access denied CREATE DATABASE
  → pmuser often has grants only on pmdb, not global CREATE.
  → On macOS Homebrew, OS user (unix_socket) is often admin:
      export PMS_DB_ADMIN_USER="$(whoami)"
      bin/run qa-bootstrap-sandbox
  → Or: PMS_DB_ADMIN_CNF=/path/to/admin.cnf
  → Linux production: create sandbox once as root, then Lab bootstrap works as pmuser:
      sudo bash /opt/pms/source/r2/deploy/ensure-pmdb-qa.sh
      # (new bootstrap-server.sh already creates pmdb_qa + GRANT)
  → Then: Lab [1] Bootstrap sandbox · [4] Seed

  "date: illegal option -- d"  (older scripts / custom code)
  → Use lib/date.sh (pms_date_month_end, pms_date_add_days, pms_date_add_months).
  → Do not call GNU date -d on macOS stock /bin/date.

  bin/run arithmetic error with "…3N"
  → Fixed via pms_date_epoch_ms in lib/date.sh (BSD date +%s%3N is not ms).

WORK ORDERS & EXPENSES (bill path)
══════════════════════════════════

  "Cannot bill: expense total is zero" but actual_cost shows money
  → actual_cost is only a field on the WO. Billing uses
    SUM(expenses WHERE work_order_id = this WO).
  → Fix: Manage work orders → select WO → N add expense (linked lines).
  → Or Work hub [6] WOs missing expenses, then [2]/N add costs.
  → Then [4] Bill completed WO (auto → paid).

  Keys on Manage work orders
  → X = list expenses linked to selected WO
  → N = add expense for this WO, then return to WO list
  → Columns #Exp and Exp$ show linked line count and sum
  → A = add WO (property + unit pickers; category ENUM only)

  No "mark paid" menu
  → Paid flips automatically on successful bill [4].
  → Expense pending/paid is a **line lock**, not a second settlement with the owner.
  → Owner recovery = ledger expense (−) from bill; package nets vs rent.

  Dashboard / Work hub shows "Status exceptions"
  → Only **money** integrity (e.g. unbilled cost on a closed WO, or ledger
    charge with WO not terminal). Not cosmetic expense.status lag.
  → Work hub [e] lists flags. Crit failures also log + telemetry
    (ops-exception); desk may open a Mark ticket. Future: OPS Fleet dashboard
    (roadmap R-M08). Do not invent a repair menu to flip statuses.

  Cosmetic expense.status lag after an old bill
  → If ledger already matches full active expense sum on a paid/invoiced WO:
    `bin/runsql expenses/heal-status-echo-lag` (any host: desk / shingle / cattle
    after day-2 code promote). Safe to re-run.

  "Unbilled cost on closed WO"
  → Active expenses > ledger charge on paid/invoiced WO.
  → Either soft-cancel the extra pending line (if not owner-chargeable),
    open a new WO for the extra work, or reverse bill [7] and re-bill the
    full honest job. Do not free-edit billed amounts.

FORUM (Commentorium)
════════════════════

  System [y] → [2] Forum

  Status bar shows: Show (all / comments / todos) · Sort (date or priority)
  F  cycle filter: all → comments only → todos only
  O  cycle sort:   newest → oldest → ★ high → ★ low
  Enter  full scrollable reader (does not edit)
  E  edit   /  search   C  clear search   ?  more keys   Q  back

  Default sort is newest first (so today’s Pip posts appear at the top).

PORTFOLIO MAP [d]→[4]
---------------------
  "Asks for Owner/Property ID but shows no list"
  → Fixed: pickers must not use id=$(pick_fn) (stdout capture hides the table).
    Update scripts/portfolio_map.sh. Expect: full OID/PID table, then prompt.

  "Screen flashes empty / vanishes"
  → Vacant filter with zero rows now shows "No vacant doors" and waits for Enter.
    Directory/job sheet clear once after data is ready.

  Same address twice in directory
  → Expected when two owners' doors share a complex. See wiki-Workflows §1.

PROPERTIES / UNITS — WHO OWNS WHAT
---------------------------------
  "Can't tell who owns a property in [p]"
  → List now shows Owner + Units. Detail pane shows Owner Name and unit count.
    Search: / owner-name   Sort: 4=Owner  5=Units

  "Owner has a shell property — how do I add a unit?"
  → [p] find PID (Units=0) → [u] A Add → enter that PID (picker shows Owner).
    See wiki-Workflows §1 "Find an owner's property".

  "Add unit: typing invisible / all prompts on one line"
  → TUI raw mode (-echo) must be restored before form entry.
    Fixed in manage_units / manage_properties (tui_restore around A/add).

  "Data truncated for column status"
  → Valid: vacant | occupied | not-ready | available  (hyphen in not-ready).
    Typos (e.g. vacabnt) are rejected before INSERT. Default blank → vacant.

TENANT + LEASE ONBOARD (vacant unit)
════════════════════════════════════

  Menu path: [v] vacant → [t] Add tenant → [l] Add lease → [r] invoice when due
  Contract detail: docs/wiki-Workflows.txt §1 · docs/workflows.txt

  "Unit still vacant after I added a lease"
  → Older builds did not sync units.status on add-lease.
  → Fixed: add/update/delete lease re-sync occupied/vacant from current
    active lease window. Pull latest; edit-save the lease once if needed.

  "Vacant list shows status occupied but unit is free"
  → Stale units.status. Fixed: [v] and lease Add run sync-unit-status-from-leases
    (occupied with no current lease → vacant). Lease Add refuses residual occupied.

  "Lease Add form: typing is invisible / all on one line"
  → TUI raw mode (stty -echo) was left on. Fixed: form leaves raw mode,
    one prompt per line, echoes each answer, confirm before create.

  "Security deposit is always 0"
  → Fixed: [l]→A defaults deposit to monthly rent (override allowed).

  "I can put two active leases on one unit"
  → Fixed: add-lease refuses a second status=active row (0-row insert);
    manage-leases pre-checks before insert.

  "Lease picker and vacant list disagree"
  → Both use the same rule: only a *current* active lease blocks the unit
    (future start_date does not hide the unit). A future active lease still
    blocks a second active lease until you terminate the first.

  "Add tenant requires email or phone"
  → Intentional: manage-tenants needs at least one contact for ops.
    (SQL import path may still insert sparse rows for tools/QA.)

  QA regression: System [y]→[Q] → [l] full or [m] smoke (re-fight OK, no wipe)
  → First visit only: seed [4] if party empty. Wipe [7]+seed [4] only if DEFEAT/corrupt.
  → Other battles: [v] AR reverse · [t] statement recon · [5] happy rent · [w] WO
  → Expect VICTORY. Log: qa/reports/last_run.jsonl

RENT COLLECTION (Process / collect rent)
════════════════════════════════════════

  Menu: [r] Rent → [3] Process / collect rent  (scripts/collect_rent.sh)

  Symptom: after amount/method, session jumps back to Rent hub and
  payment is NOT recorded.
  → Older build exited on empty method or validation errors (hard exit).
  → Fixed: validation stays on the collect screen; method defaults to ONLINE
    if you press Enter; explicit "Confirm receipt" then [Y/n] before post.
  → Pull latest main / re-run setup-r2.sh; open a fresh /r2/ session.

  Flow (current):
    1. Pick invoice number (or 0 to exit)
    2. Amount (Enter = full balance)
    3. Method (Enter = ONLINE) — CHECK/WIRE/ZELLE/CASH/ONLINE
    4. Optional ref + notes
    5. Review "Confirm receipt" → type Y or YES to record
  → Payment + owner ledger (rent_collected + management_fee) post together.

  Still fails after confirm?
  → Read on-screen "Transaction failed" / ledger warning (do not double-pay).
  → Check MariaDB as pmuser; verify invoice still unpaid.

DOUBLE PAYMENT / COLLECTED > INVOICED ON DASHBOARD
══════════════════════════════════════════════════

  Symptom: General dashboard shows Rent Collected YTD higher than Rent Invoiced YTD.
  Cause: usually duplicate payment rows on the same invoice (operator double-entry),
  or status drift (paid with no payments / balance_owed wrong). Late fees can also
  make cash > base amount; that is smaller and expected.

  Fix (product path — do not DELETE FROM payments):
  1. Pull latest code + run migrations (needs 1.20 payment soft-void columns):
       cd ~/Projects/property_management/r2   # or server path
       ./migrations/migrate.sh up
  2. Rent hub [r] → [4] AR exceptions / reverse pay
  3. Open the flagged invoice (OVERPAID / MULTI_PAY)
  4. [R] reverse the phantom payment(s) — reason DUPLICATE_ENTRY, type YES
  5. Confirm balance/status look right; use [F] if only status needs recompute
  6. Regenerate owner statements for months that included the bad payment

  Dashboard “Rent Collected” counts only payments.status = posted.
  General dashboard also shows an AR Exceptions badge when any flags exist.

  Collect path now warns if you try to pay an already-paid invoice again.

REVERSE SHOWS status=2000.00 / ref=posted / “STILL POSTED”
══════════════════════════════════════════════════════════

  Symptom (old bug, fixed 2026-07-16):
    · Confirmation: Invoice status=4000.00, ref=posted
    · Payment list: STATUS column shows Y (that was LEDGER, columns shifted)
    · After YES: “Payment still posted — reverse did not apply (status=2000.00)”
  Cause: bash collapsed empty TSV fields (blank payments.reference). Soft-void
    often DID apply; ledger clear + invoice recompute were skipped by the bad check.
  Fix: pull latest main (pms_tsv_assign in lib/runsql.sh + ar_exceptions.sh).
    Exit the AR exceptions screen fully and re-open so the new script loads.

  Recover if payment is already reversed but ledger still shows Y:
    1. [r]→[4] → open invoice → select that payment
    2. Accept “already reversed — continue cleanup?” → clears ledger + recomputes
    3. Or verify:
         mariadb -u pmuser -D pmdb -e \
           "SELECT payment_id,status,reverse_reason FROM payments WHERE payment_id=N;"
         # ledger for that pay should be 0 rows after cleanup:
         # SELECT COUNT(*) FROM owner_transactions
         #  WHERE reference_type='payment' AND reference_id=N;

  Confirm UI is healthy after pull:
    Payment list STATUS = posted|reversed (not Y)
    LEDGER column = Y|N
    Reverse confirm shows invoice status=paid|issued|… (word, not dollars)

AR EXCEPTIONS MENU FAILS / “UNKNOWN COLUMN status”
══════════════════════════════════════════════════

  Symptom: [r]→[4] errors, empty list, or SQL “Unknown column 'status'”.
  Cause: migration 1.20 not applied (payments lacks status / reverse_*).
  Fix:
       cd …/property_management/r2
       ./migrations/migrate.sh up
       mariadb -u pmuser -D pmdb -e "SHOW COLUMNS FROM payments LIKE 'status';"
  Expect: status column DEFAULT 'posted'. Then re-open [r]→[4].

  Symptom: Access denied for user 'pmuser' (using password: NO)
  Cause: no MariaDB credentials for the OS user running the menu.
  Fix: copy/create ~/.my.cnf (mode 600) with [client] user/password,
       or use production /etc/pms/db.conf + pmuser home .my.cnf.

AR AGING “NOT AUTHORIZED” / DISABLED
════════════════════════════════════

  Symptom: [r]→[6] or [s]→[6] → registered but disabled (not authorized).
  Fix: pull latest main (ar-aging + reports/ar-aging enabled=1).
  CLI smoke: bin/run ar-aging
  Empty report with green “clear” is OK when nothing is overdue.

  Layout (2026-07-16): compact ~80-col grid — Tenant, Property, Unit, Due,
  Age, Owed, Bkt. Orig/Paid totals on footer. Long lines that wrap on ttyd
  mean an older ar_aging.sh is still loaded — pull + re-run.

OWNER STATEMENT PDFs
════════════════════

  Owner packages: CSV + Markdown + PDF (when tools present).
  Tenant monthly: CSV only (no PDF pipeline yet).
  Periods:
      YYYY-MM     month
      YYYY-Qn     quarter
      YTD / YYYY-YTD   year-to-date (Jan 1 → today; prior year → Dec 31)
      YYYY        full calendar year
  Hub: [s]→[o]  keys [t]/[T] YTD · [j] owner portfolio job sheet
  QA Lab writes under qa/artifacts/… ; MANIFEST.txt has pdf_files=0 if PDF skipped.
  Production: reports/owner_statements/<period>/

  "PDF skipped — install pandoc + xelatex/pdflatex"
  → Need both pandoc and a LaTeX engine.

  macOS (BasicTeX or MacTeX) — pull code on MacBook and review PDFs:
      brew install pandoc
      brew install --cask basictex
  → Put TeX on PATH (new terminal or):
      eval "$(/usr/libexec/path_helper)"
      # or: export PATH="/Library/TeX/texbin:$PATH"
  → Generator also auto-prepends /Library/TeX/texbin and Homebrew pandoc when missing.
  → Verify:  which pandoc pdflatex xelatex tlmgr
      expect pandoc (brew) and /Library/TeX/texbin/...
  → Then (with local MariaDB + ~/.my.cnf):
      cd …/property_management/r2
      bin/run owner-statements-generate YTD
      open reports/owner_statements/$(date +%Y)-YTD/*.pdf

  "sudo: tlmgr: command not found"
  → BasicTeX is installed, but sudo's secure PATH omits /Library/TeX/texbin.
  → Always call the full path:
      sudo /Library/TeX/texbin/tlmgr update --self
      sudo /Library/TeX/texbin/tlmgr install collection-fontsrecommended \
        fancyhdr xcolor booktabs longtable setspace hyperref
  → Same packages power templates/owner_statement_header.tex (letterhead chrome).

  Linux production (missing TeX on an older droplet):
      sudo apt-get install -y pandoc texlive-xetex \
        texlive-latex-recommended texlive-fonts-recommended texlive-latex-extra
      command -v pandoc xelatex
  → Fresh bootstrap (deploy/bootstrap-server.sh and steps/01) installs this set.
  → Re-generate: bin/run owner-statements-generate 2026-Q2
  → Or YTD: bin/run owner-statements-generate YTD

  PDF tools present but no PDF / empty pdf_files
  → Look for *.pandoc.log next to the intended PDF in the output dir.
  → Install any missing LaTeX package named in that log via tlmgr (full path).

  Re-generate after fixing tools (QA example):
      export PM_QA_MODE=1 PMS_DB_NAME=pmdb_qa
      bin/run owner-statements-generate 2025-Q1
      open qa/artifacts/owner_statements/2025-Q1/

  Headless Linux (SSH) has no PDF viewer
  → Generate on Linux or Mac; review PDFs on MacBook after git pull.
  → open / Preview.app: open reports/owner_statements/…/*.pdf

DOCUMENTATION ISSUES
════════════════════

  Wiki shows old table list
  → The wiki reads from docs/wiki-*.txt (not scripts/docs/).
  → Safe:  cd scripts && ./update_docs.sh   # schema only; curated copy protected
  → Policy: docs/wiki-CURATED.txt

  "File not found" in a wiki topic
  → Schema files: ./update_docs.sh
  → Other topics are hand-maintained (or restore from git) — not auto-created.

  I accidentally overwrote Features/Workflows/…
  → Only possible with --overwrite-curated after typing OVERWRITE or --yes.
  → Restore:  git checkout HEAD -- docs/wiki-Main_Features.txt  (etc.)

FORUM / COMMENTORIUM
════════════════════

  System → [2] Forum (or bin/run commentorium)

  "declare: -A: invalid option" / "bad substitution" / blank forum on macOS
  → Older commentorium used Bash 4+ features (declare -A, ${var^^}).
  → Fixed for Bash 3.2 (macOS stock /bin/bash) — pull latest main and retry.
  → Data lives in table notes (migration 1.11), not a table named commentorium.

  Arrow keys print ^[[A / ^[[B or break the screen
  → All interactive lists must use lib/tui.sh:  read_key; key="${TUI_KEY}"
    (never key=$(read_key) — subshell breaks tty ESC reads on macOS).
  → Do not use read -t 0.3 for arrow sequences on Bash 3.2 (fractional -t is Bash 4+).
    lib/tui.sh uses stty min/time (tenths of a second) for CSI tails instead.
  → Do not keep a local read_key() in manage_* scripts — use the shared one.
  → Same pattern: manage_owners / manage_units / manage_workorders / commentorium.

  Forum opens but empty list
  → SELECT COUNT(*) FROM notes;  — if 0, seed via migration 1.11 or add with N=New.
  → Confirm DB: PMS_DB_NAME (default pmdb) and user pmuser can read notes.

MENU & TERMINAL ISSUES
══════════════════════

  Arrow keys don't work / print escape codes on macOS
  → Requires lib/tui.sh + tui_setup (raw mode so the terminal does not echo ESC sequences).
  → Run scripts via shebang/bash (./scripts/pm-menu.sh or bin/run …). You do NOT need
    to change your login shell from zsh to bash — scripts force bash via #!/usr/bin/env bash.
  → Stock macOS /bin/bash is 3.2 — avoid declare -A and fractional read -t in TUI code.
  → Run from a real terminal (Terminal.app, iTerm, Linux tty). Not all web terminals
    support raw key sequences — see docs/ttyd-troubleshooting.txt for ttyd/web setup.
  → Smoke-check: open Work Orders or Owners and press ↑/↓ — highlight must move without
    printing ^[[A / ^[[B.

  Colors look wrong
  → Run scripts/test-colors.sh to validate your terminal.
  → Scripts source lib/colors.sh for cross-platform ANSI codes.

  "Command not found: mariadb"
  → Install MariaDB client:  pacman -S mariadb-clients  (Arch) / apt install mariadb-client

SCRIPT ERRORS
═════════════

  "SQL file not found"
  → Check sql/ directory; db_helper.sh loads from sql/workorders/ for work order queries.

  Report CSV is empty
  → Verify active leases exist and invoices were generated for the target month.

GETTING HELP
════════════
  • [wi] Wiki — browse all topics
  • [fo] Forum — leave a note in the notes table
  • Wiki → Mission & Services — customer-facing overview
  • docs/strategy.txt — public north star (architecture & direction)
  • Wiki → Internal Strategy — operator planning (PIN required)
  • docs/pms.file_folder_layout.txt — production deployment layout