← 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