System

Activity Log

Unified timeline of data changes (saves, imports, deletes) and backend errors. Filter by type, area, severity, or search.

Data changes (total)1,029
Errors (total)216,966
Today · changes0
Today · errors45,872
Reset

6,953 results · Page 140 of 140

documents · upload
2026-04-16 14:20:19 · anonymous · /backend/documents.php
change
backend_document #18
Context
{"file_name":"PAY_BILLS_ENHANCED_FEATURES.md","mime_type":"application/octet-stream"}
Before
[]
After
{"backend_document_id":"18","document_type":"upload","title":"Open Code 04-16 Pay Bills changes","slug":"open-code-04-16-pay-bills-changes","summary_text":"Open Code 04-16 Pay Bills changes","content_markdown":null,"content_html":null,"file_name":"PAY_BILLS_ENHANCED_FEATURES.md","stored_name":"20260416-182019-18adea90.md","mime_type":"application/octet-stream","file_size_bytes":"8500","storage_path":"/mnt/drive1/customerdb/backend/documents_storage/20260416-182019-18adea90.md","is_deleted":"0","created_at":"2026-04-16 14:20:19","updated_at":"2026-04-16 14:20:19","editor_content":"# Pay Bills - Enhanced Features Documentation\n\n**Last Updated:** April 16, 2026  \n**File:** `/backend/bills.php`  \n**Server:** `kefa@192.168.7.202`  \n**Data Location:** `/mnt/drive1/customerdb/`\n\n---\n\n## Overview\n\nThe Pay Bills page has been enhanced with new features for better bank account management and payment tracking visibility.\n\n---\n\n## New Features\n\n### 1. Bank Account Types\n\nEach bank account now has a **type classification**:\n\n| Type | Icon | Description |\n|------|------|-------------|\n| **Pay Bill** | 💳 | Used for paying bills - appears in bank dropdown |\n| **Show Balance** | 👁 | Track only - excluded from bill payment dropdowns |\n\n**Why this matters:** Accounts marked as \"Show Balance\" won't clutter the bill payment dropdown but still track their running balance.\n\n### 2. Bank URL (Popup Window)\n\nEach account can have a **Bank URL** that opens in a popup window.\n\n- Click the \"🌐 Bank\" button next to any account\n- Opens the URL in a 1200x800 popup window\n- Useful for quick access to bank login without leaving the bills page\n\n### 3. Bal After Pills (Status Badges)\n\nThe \"Bal After\" column now shows **color-coded pills**:\n\n| Status | Appearance | Meaning |\n|--------|------------|---------|\n| **Paid** | Green pill with ✓ | Bill amount has been paid |\n| **Due** | Red pill with ! | Still needs to be paid |\n| **—** | Gray pill | No amount entered |\n\n### 4. Save All Buttons\n\nTwo save buttons added for convenience:\n\n1. **Save All Balances** - In the bank accounts section\n2. **Save All Bills** - At the bottom of the bills table\n\nBoth show \"Saving…\" → \"Saved ✓\" feedback.\n\n### 5. Auto-Update\n\nAll changes auto-save after 600ms of inactivity (debounced).\n\n---\n\n## Database Schema Changes\n\n### New Columns in `bills_accounts` Table\n\n```sql\n-- MySQL / MariaDB\nALTER TABLE bills_accounts\nADD COLUMN account_type ENUM('pay_bill', 'show_balance') DEFAULT 'pay_bill' AFTER name,\nADD COLUMN bank_url TEXT DEFAULT '' AFTER account_type;\n\n-- SQLite\nALTER TABLE bills_accounts ADD COLUMN account_type TEXT DEFAULT 'pay_bill';\nALTER TABLE bills_accounts ADD COLUMN bank_url TEXT DEFAULT '';\n```\n\n### New Tables Created\n\nIf tables don't exist, the following are auto-created:\n\n```sql\nCREATE TABLE bills_accounts (\n    id INTEGER PRIMARY KEY AUTOINCREMENT,\n    name TEXT NOT NULL,\n    account_type TEXT DEFAULT 'pay_bill',\n    bank_url TEXT DEFAULT '',\n    sort_order INTEGER DEFAULT 0,\n    created_at TEXT DEFAULT CURRENT_TIMESTAMP\n);\n\nCREATE TABLE bills_monthly (\n    id INTEGER PRIMARY KEY AUTOINCREMENT,\n    year INTEGER NOT NULL,\n    month INTEGER NOT NULL,\n    bills_account_id INTEGER,\n    opening_balance REAL DEFAULT 0,\n    UNIQUE(year, month, bills_account_id)\n);\n\nCREATE TABLE bills_items (\n    id INTEGER PRIMARY KEY AUTOINCREMENT,\n    name TEXT NOT NULL,\n    default_due REAL DEFAULT 0,\n    notes TEXT DEFAULT '',\n    sort_order INTEGER DEFAULT 0,\n    created_at TEXT DEFAULT CURRENT_TIMESTAMP\n);\n\nCREATE TABLE bills_entries (\n    id INTEGER PRIMARY KEY AUTOINCREMENT,\n    year INTEGER NOT NULL,\n    month INTEGER NOT NULL,\n    bills_item_id INTEGER NOT NULL,\n    bills_account_id INTEGER DEFAULT 0,\n    payment_due REAL DEFAULT 0,\n    amount_paid REAL DEFAULT 0,\n    is_paid INTEGER DEFAULT 0,\n    paid_date TEXT DEFAULT '',\n    UNIQUE(year, month, bills_item_id)\n);\n\nCREATE TABLE bills_copy_log (\n    id INTEGER PRIMARY KEY AUTOINCREMENT,\n    year INTEGER NOT NULL,\n    month INTEGER NOT NULL,\n    copied_from_year INTEGER NOT NULL,\n    copied_from_month INTEGER NOT NULL,\n    copied_at TEXT DEFAULT CURRENT_TIMESTAMP\n);\n```\n\n---\n\n## Deployment Instructions\n\n### Step 1: Upload the File\n\n**Option A: Direct Copy (if file is on local machine)**\n\n```bash\n# From local machine to server\nscp bills.php kefa@192.168.7.202:/var/www/floridaalterations.com/backend/bills.php\n```\n\n**Option B: If editing on server directly**\n\n```bash\nssh kefa@192.168.7.202\n# Then edit the file at:\n# /var/www/floridaalterations.com/backend/bills.php\n```\n\n### Step 2: Set Permissions\n\n```bash\nssh kefa@192.168.202\nchmod 644 /var/www/floridaalterations.com/backend/bills.php\nchown www-data:www-data /var/www/floridaalterations.com/backend/bills.php\n```\n\n### Step 3: Clear Cache (Optional)\n\n```bash\n# If using OPcache\nssh kefa@192.168.7.202\nphp -r \"opcache_get_status();\" 2>/dev/null && echo \"Consider: php-fpm restart\"\n```\n\n### Step 4: Access the Page\n\n```\nhttps://ella.floridaalterations.com/backend/bills.php\n```\n\n---\n\n## How to Use\n\n### Adding a New Account with Bank URL\n\n1. Click **\"+ Add Account\"** button\n2. Enter account name (e.g., \"Chase Checking\")\n3. Select account type:\n   - **Pay Bill** - For accounts you'll pay bills from\n   - **Show Balance** - For tracking only\n4. Enter Bank URL (e.g., `https://www.chase.com/login`)\n5. Click **\"Add Account\"**\n6. Enter the opening balance and click **\"Save Balances\"**\n\n### Editing an Existing Account\n\n1. Click **\"Edit\"** next to the account\n2. Update name, type, or URL\n3. Click **\"Save Changes\"**\n\n### Switching Account Type\n\n1. Edit the account\n2. Change the radio button selection\n3. Click **\"Save Changes\"**\n\n### Opening Bank in Popup\n\n1. Click the **\"🌐 Bank\"** button next to an account\n2. The bank URL opens in a new popup window\n\n### Marking a Bill as Paid\n\n1. Enter the **Paid ($)** amount (or it auto-calculates)\n2. The **Bal After** pill turns **green** with a ✓ checkmark\n3. Row background turns light blue to indicate paid status\n\n### Viewing Running Balance\n\n- The sidebar shows **Running Totals** for each pay-bill account\n- Updates automatically as you enter payments\n\n---\n\n## File Location Reference\n\n| Item | Server Path |\n|------|-------------|\n| bills.php | `/var/www/floridaalterations.com/backend/bills.php` |\n| Data directory | `/mnt/drive1/customerdb/` |\n| Database | `/mnt/drive1/customerdb/customer.db` |\n| Backup | `/mnt/drive1/customerdb/backup/` |\n| Logs | `/mnt/drive1/customerdb/logs/` |\n\n---\n\n## Troubleshooting\n\n### Database Connection Error\n\n```\nError: Connection failed\n```\n\n**Solution:**\n```bash\n# Check if MySQL is running\nssh kefa@192.168.7.202\nsudo systemctl status mysql\n# or\nsudo systemctl status mariadb\n```\n\n### Table Doesn't Exist\n\nIf you see errors about missing tables:\n\n```bash\n# The tables are auto-created on first page load\n# Just refresh the page once\n```\n\n### Bank URL Not Opening\n\n- Verify the URL includes `https://` or `http://`\n- Check browser popup blocker settings\n- Some banking sites block popup windows\n\n### Bal After Shows \"—\"\n\n**Causes:**\n1. No bank account selected for the bill\n2. Opening balance not saved for the account\n3. Account is marked as \"Show Balance\" type\n\n**Solution:**\n1. Select a bank account from the dropdown\n2. Save the opening balance for that account\n\n### Changes Not Saving\n\n1. Check browser console for JavaScript errors\n2. Verify network tab shows POST requests completing\n3. Clear browser cache and retry\n\n---\n\n## Quick Commands Reference\n\n```bash\n# Connect to server\nssh kefa@192.168.7.202\n\n# View current bills.php\ncat /var/www/floridaalterations.com/backend/bills.php | head -50\n\n# Backup bills.php before updating\ncp /var/www/floridaalterations.com/backend/bills.php /mnt/drive1/customerdb/backup/bills.php.$(date +%Y%m%d)\n\n# Upload new file\nscp bills.php kefa@192.168.7.202:/var/www/floridaalterations.com/backend/bills.php\n\n# Set correct permissions\nchmod 644 /var/www/floridaalterations.com/backend/bills.php\nchown www-data:www-data /var/www/floridaalterations.com/backend/bills.php\n\n# Check PHP error logs\nssh kefa@192.168.7.202 \"tail -50 /var/log/php*-fpm.log\"\n\n# Restart PHP-FPM if needed\nssh kefa@192.168.7.202 \"sudo systemctl restart php*-fpm\"\n```\n\n---\n\n## Security Notes\n\n- Bank URLs are stored as-is - ensure only trusted users can edit accounts\n- No sensitive data encrypted in the database\n- Consider HTTPS-only for all bank URLs\n- Session-based access control should be configured at the application level\n\n---\n\n## Future Enhancements (Suggested)\n\n1. **Auto-fetch balance** - API integration with banks (requires bank credentials)\n2. **Email notifications** - Remind when bills are due\n3. **Export to CSV** - Download bill history\n4. **Recurring bills** - Auto-create monthly bills\n5. **Budget tracking** - Set monthly budget limits per category\n\n---\n\n## Version History\n\n| Version | Date | Changes |\n|---------|------|---------|\n| 1.0 | April 16, 2026 | Initial release with account types, bank URLs, status pills |\n| - | - | Auto-save functionality |\n| - | - | Save All buttons added |\n\n---\n\n*End of Documentation*\n","is_text_editable":1,"can_edit_inline":1}
documents · save
2026-04-07 15:08:09 · anonymous · /backend/documents.php?backend_document_id=10
change
backend_document #10
Context
{"document_type":"upload"}
Before
{"backend_document_id":"10","document_type":"upload","title":"Claude 04-07-2026","slug":"claude-04-07-2026","summary_text":"Claude 04-07-2026 fixing backend Document areas","content_markdown":null,"content_html":null,"file_name":"SESSION_LOG_2026-04-07.md","stored_name":"20260407-190624-56f0e818.md","mime_type":"application/octet-stream","file_size_bytes":"21112","storage_path":"/mnt/drive1/customerdb/backend/documents_storage/20260407-190624-56f0e818.md","is_deleted":"0","created_at":"2026-04-07 15:06:24","updated_at":"2026-04-07 15:06:24","editor_content":"# Session Log — 2026-04-07\n\nAll work completed in this session on the CustomerDB backend application.\n\n---\n\n## Summary of New Modules and Changes\n\n| Area | File(s) | Status |\n|------|---------|--------|\n| Bills Manager | `backend/bills_service.php`, `backend/bills.php` | New |\n| Next-Day Email Job | `backend/jobs/send_nextday_customer_emails.php` | New |\n| Cron Manager UI | `backend/cron_manager.php` | New |\n| Error Log UI | `backend/error_log.php` | New |\n| Change Log UI | `backend/change_log.php` | New |\n| Stocks & Crypto | `backend/stocks_crypto_service.php`, `backend/stocks_crypto.php` | New |\n| Backend Home | `backend/index.php` | Modified |\n| Documents Editor | `backend/documents.php` | Full rewrite |\n| Documents Service | `backend/document_service.php` | Bug fix |\n\n---\n\n## 1. Bills Manager\n\n### Files\n- `backend/bills_service.php` — 356 lines, service layer\n- `backend/bills.php` — 972 lines, UI page\n\n### Purpose\nReplaces a spreadsheet (`newbills2026.xlsx`) with a web-based bill payment tracker.\nTracks bank account opening balances, monthly bill payments, and computes running bank\nbalances in the same layout as the Excel spreadsheet.\n\n### Database Tables (auto-created on first load)\n\n**`bills_account`**\n| Column | Type | Notes |\n|--------|------|-------|\n| `bills_account_id` | INT PK AUTO | |\n| `name` | VARCHAR(120) | Bank name |\n| `sort_order` | INT | Display order |\n\n**`bills_item`**\n| Column | Type | Notes |\n|--------|------|-------|\n| `bills_item_id` | INT PK AUTO | |\n| `label` | VARCHAR(200) | Bill name |\n| `default_amount` | DECIMAL(12,2) | Pre-fills the Due column |\n| `default_account_id` | INT | Default bank to debit |\n| `notes` | TEXT | Instructions / URLs |\n| `sort_order` | INT | Row order |\n| `is_deleted` | TINYINT | Soft delete |\n\n**`bills_entry`**\nMonthly bill payment rows.\n\n| Column | Type | Notes |\n|--------|------|-------|\n| `bills_entry_id` | INT PK AUTO | |\n| `bills_item_id` | INT | FK to `bills_item` |\n| `year`, `month` | INT | Billing period |\n| `amount_due` | DECIMAL(12,2) | Amount billed |\n| `amount_paid` | DECIMAL(12,2) | Amount actually paid |\n| `account_id` | INT | Bank used to pay |\n\n**`bills_balance`**\nOpening bank balances per month.\n\n| Column | Type | Notes |\n|--------|------|-------|\n| `bills_balance_id` | INT PK AUTO | |\n| `account_id` | INT | FK to `bills_account` |\n| `year`, `month` | INT | Period |\n| `opening_balance` | DECIMAL(14,2) | Balance at start of month |\n\n### Seed Data\nOn first load (empty table), the following are auto-seeded from the original Excel file:\n\n**Accounts:** USAA, MIDFLORIDA, Centenial, CapitalOne\n\n**Bills (11 items):** MidFlorida Mortgage, MidFlorida Credit Card, USAA Credit Card,\nCentenial Bank, Electricity, Waste Management, Comcast/Xfinity, Insurance, Water,\nCapitalOne #1, CapitalOne #2\n\n### Service Functions\n\n```php\napp_install_bills_schema(mysqli $conn): void\napp_bills_seed_defaults(mysqli $conn): void\napp_bills_accounts(mysqli $conn): array\napp_bills_all_accounts(mysqli $conn): array\napp_bills_items(mysqli $conn): array\napp_bills_balances(mysqli $conn, int $year, int $month): array       // map: account_id → float\napp_bills_entries(mysqli $conn, int $year, int $month): array        // map: item_id → [due, paid, account_id]\napp_bills_active_months(mysqli $conn, int $year): array\napp_bills_save_balance(mysqli $conn, int $year, int $month, int $accountId, float $balance): void\napp_bills_save_entry(mysqli $conn, int $year, int $month, int $itemId, ?float $due, ?float $paid, ?int $accountId): void\napp_bills_save_account(mysqli $conn, array $data): int\napp_bills_delete_account(mysqli $conn, int $id): void\napp_bills_save_item(mysqli $conn, array $data): int\napp_bills_delete_item(mysqli $conn, int $id): void\napp_bills_reorder_item(mysqli $conn, int $id, string $dir): void\napp_bills_copy_month(mysqli $conn, int $fromYear, int $fromMonth, int $toYear, int $toMonth): int\n```\n\n### UI Features (`bills.php`)\n- Month/year navigation with ◀ / ▶ buttons and active-month pills\n- **Bank Balances section** — editable opening balance per account, inline Save button\n- **Bills table** — spreadsheet-style rows with:\n  - Amount Due (editable, pre-filled from default)\n  - Amount Paid (editable, AJAX auto-save after 600 ms debounce)\n  - Account selector (which bank pays this bill)\n  - Balance After column (computed client-side in real time)\n  - Up/Down sort buttons\n  - Edit and Delete per row\n- **Sticky running totals sidebar** — shows each bank's current balance as bills are entered\n- **Copy from previous month** — modal to copy all entries from a prior month to the current one\n- **Add Bill / Edit Bill modals**\n- **Add Account / Edit Account modals**\n- All saves use POST/Redirect/GET; bill entry changes auto-save via `fetch()` AJAX\n\n### Running Total Calculation\nJavaScript function `recalcTotals()` walks bills in DOM order, maintains a\n`bal[accountId]` map starting from opening balances, subtracts `amount_paid` from the\nselected account for each bill, and updates the \"Balance After\" column and sidebar panel\nin real time — matching the Excel spreadsheet formula logic.\n\n---\n\n## 2. Next-Day Customer Email Job\n\n### File\n- `backend/jobs/send_nextday_customer_emails.php` — 158 lines\n\n### Purpose\nPHP CLI translation of the VBA macro `RunNextDayCustomerEmails()`.\nQueries `setmore_appointments` for appointments scheduled for tomorrow\n(status ≠ Cancelled) and sends a reminder email to each customer.\n\n### Usage\n```bash\n# Normal run (sends real emails)\nphp backend/jobs/send_nextday_customer_emails.php\n\n# Dry run — prints what would be sent, no emails sent\nphp backend/jobs/send_nextday_customer_emails.php --dry-run\n\n# Override date (for testing a specific date)\nphp backend/jobs/send_nextday_customer_emails.php --date=2026-04-09\n```\n\n### Recommended Cron Schedule\n```\n0 20 * * *  php /path/to/backend/jobs/send_nextday_customer_emails.php\n```\nRuns at 8:00 PM every evening, sends reminders for the following day's appointments.\n\n### Email Content\n- Subject: `Reminder: Your appointment at Ella's Alterations tomorrow`\n- Body matches the original VBA email with full address, phone, 5-line award\n  signature block, and a plain-text fallback\n- Uses `app_send_mail()` / PHPMailer\n\n---\n\n## 3. Cron Manager UI\n\n### File\n- `backend/cron_manager.php` — 482 lines\n\n### Purpose\nWeb UI for managing scheduled background jobs. Stores job definitions in the database,\nallows toggling on/off, running immediately, editing schedules, and exporting to\n`/etc/cron.d/` format.\n\n### Database Table (auto-created)\n\n**`cron_job`**\n| Column | Type | Notes |\n|--------|------|-------|\n| `cron_job_id` | INT PK AUTO | |\n| `label` | VARCHAR(120) | Human name |\n| `description` | TEXT | What the job does |\n| `schedule` | VARCHAR(100) | Cron expression e.g. `0 20 * * *` |\n| `command` | VARCHAR(500) | Shell command to run |\n| `enabled` | TINYINT | 0 = disabled, 1 = enabled |\n| `last_run_at` | DATETIME | Updated on \"Run Now\" |\n| `last_output` | TEXT | Captured stdout/stderr |\n| `sort_order` | INT | Display order |\n\n### Seeded Jobs (on first install)\n1. **Next-Day Customer Emails** — `0 20 * * *` — `php backend/jobs/send_nextday_customer_emails.php`\n2. **Morning Jobs** — `0 7 * * *` — `php backend/jobs/morning_jobs.php`\n3. **Nightly Reports** — `0 23 * * *` — `php backend/jobs/nightly_reports.php`\n4. **DB Backup** — `0 2 * * *` — `bash backend/bin/db_backup.sh`\n5. **Daily Cleanup** — `30 3 * * *` — `php backend/jobs/daily_cleanup.php`\n6. **Add Name Records Auto Delete** — `0 4 * * *` — `php backend/jobs/cleanup_name_records.php`\n\nNew jobs are added idempotently (checked by label before inserting) so existing\ninstallations are not re-seeded.\n\n### UI Features\n- Table listing all jobs with schedule, enabled toggle, last run time\n- **Toggle** — flip enabled/disabled immediately (POST/redirect)\n- **▶ Run Now** — executes the command via `exec()`, captures output, stores to DB, shows in modal\n- **Add / Edit** — form with label, description, command, schedule, enabled flag\n- **Schedule presets** — quick-select buttons (hourly, daily at common times, weekly, monthly)\n- `cron_describe(string $expr): string` — converts `0 20 * * *` → \"Daily at 20:00\"\n- **Export Crontab** — writes `/tmp/customerdb_crontab_export.txt` in `/etc/cron.d/` format\n\n---\n\n## 4. Error Log UI\n\n### File\n- `backend/error_log.php` — 262 lines\n\n### Purpose\nDedicated UI for browsing `backend_error_log` records with filtering, pagination,\nand per-entry management.\n\n### Features\n- **Stats bar** — Total errors, Today's errors, Latest error timestamp\n- **Filter bar** — Search (message/context), Source dropdown, Severity dropdown\n- **Pagination** — 50 per page with smart ellipsis navigator\n- **Per-row delete** — removes individual log entries\n- **Clear All button** — truncates the entire log (with confirmation)\n- JSON context blocks displayed in `<details>` collapsibles with dark monospace styling\n- URL: `/backend/error_log.php`\n\n---\n\n## 5. Change Log UI\n\n### File\n- `backend/change_log.php` — 248 lines\n\n### Purpose\nDedicated UI for browsing `backend_change_log` records — audit trail of all\ndata changes across the application.\n\n### Features\n- **Filter bar** — Search, Area, Action type, Entity type dropdowns\n- **Pagination** — smart ellipsis navigator\n- **Per-row detail panels:**\n  - Before / After / Context — collapsed JSON blocks\n  - Changed Data — open by default, shows the changed fields summary\n- Action badges color-coded by type (create, update, delete)\n- URL: `/backend/change_log.php`\n\n---\n\n## 6. Stocks & Crypto Module\n\n### Files\n- `backend/stocks_crypto_service.php` — 269 lines, service layer\n- `backend/stocks_crypto.php` — 667 lines, UI page\n\n### Purpose\nTrack a personal investment portfolio — individual stocks, ETFs, precious metals,\nand crypto. Supports live price fetching, manual daily price entry, monthly crypto\nbuy tracking, and portfolio gain/loss analysis.\n\n### Database Tables (auto-created)\n\n**`stocks_asset`**\n| Column | Type | Notes |\n|--------|------|-------|\n| `stocks_asset_id` | INT PK AUTO | |\n| `ticker` | VARCHAR(20) | e.g. `AAPL`, `BTC`, `GLD` |\n| `name` | VARCHAR(120) | Display name |\n| `asset_type` | ENUM | `stock`, `etf`, `crypto`, `metal`, `other` |\n| `shares` | DECIMAL(18,8) | Quantity owned |\n| `cost_basis_total` | DECIMAL(14,6) | Total purchase cost |\n| `is_active` | TINYINT | Soft hide |\n\n**`stocks_price`**\nDaily price records.\n\n| Column | Type | Notes |\n|--------|------|-------|\n| `stocks_price_id` | INT PK AUTO | |\n| `ticker` | VARCHAR(20) | |\n| `price_date` | DATE | |\n| `price` | DECIMAL(14,6) | |\n| `source` | VARCHAR(40) | `manual` or `yahoo` |\n\nUnique constraint on `(ticker, price_date)` — upserts with `ON DUPLICATE KEY UPDATE`.\n\n**`crypto_monthly_buy`**\n| Column | Type | Notes |\n|--------|------|-------|\n| `crypto_monthly_buy_id` | INT PK AUTO | |\n| `ticker` | VARCHAR(20) | |\n| `year`, `month` | INT | |\n| `quantity` | DECIMAL(18,8) | Crypto units purchased |\n| `amount_usd` | DECIMAL(14,2) | USD spent |\n| `notes` | VARCHAR(255) | |\n\n### Service Functions\n\n```php\napp_install_stocks_schema(mysqli $conn): void\napp_stocks_assets(mysqli $conn, string $type = ''): array\napp_stocks_save_asset(mysqli $conn, array $data): int\napp_stocks_delete_asset(mysqli $conn, int $id): void\napp_stocks_save_price(mysqli $conn, string $ticker, string $date, float $price, string $source = 'manual'): void\napp_stocks_save_prices_bulk(mysqli $conn, string $date, array $prices): int\napp_stocks_latest_prices(mysqli $conn): array                          // map: ticker → [price, date]\napp_stocks_price_history(mysqli $conn, string $ticker, int $limit = 90): array\napp_stocks_fetch_live_price(string $ticker): ?float                    // Yahoo Finance API, 8s timeout\napp_stocks_fetch_all_live(mysqli $conn): array                         // fetches all active tickers\napp_crypto_monthly_buys(mysqli $conn, int $year, int $month = 0): array\napp_crypto_save_monthly_buy(mysqli $conn, array $data): void\napp_crypto_delete_monthly_buy(mysqli $conn, int $id): void\napp_stocks_dashboard(mysqli $conn): array                              // cost_basis, current_value, gain_loss, gain_pct per asset\napp_stocks_stats(mysqli $conn): array                                  // total portfolio stats\n```\n\n### Live Price Source\nYahoo Finance unofficial API:\n```\nhttps://query1.finance.yahoo.com/v8/finance/chart/{TICKER}?interval=1d&range=1d\n```\n8-second timeout, reads `.chart.result[0].meta.regularMarketPrice`.\n\n### UI Tabs (`stocks_crypto.php`)\n\n1. **Portfolio** — dashboard table with ticker, shares, cost basis, current value,\n   gain/loss, gain %, latest price date. Stats bar shows total value, total cost,\n   total gain/loss. \"Fetch Live Prices\" button calls Yahoo Finance for all tickers.\n\n2. **Enter Prices** — manual daily price entry grid for all active assets,\n   date picker (defaults to today), bulk save.\n\n3. **Crypto Buys** — monthly crypto purchase log. Month/year selector, table of\n   ticker/quantity/USD/notes rows, add and delete per entry.\n\n4. **Add Asset** — form to add a new stock, ETF, crypto, metal, or other asset.\n\n### Auto-Refresh\nA checkbox above the stats bar auto-refreshes the Portfolio tab every 10 seconds.\nState persists in `localStorage` key `sc_auto_refresh`. On reload, strips `?ok=`\nand `?warn=` query params to prevent the notice from repeating every 10 seconds.\n\n---\n\n## 7. Backend Home — New Cards (`index.php`)\n\nSix new cards added to the backend home dashboard:\n\n| Card | Link | Secondary Action |\n|------|------|-----------------|\n| Customer Totals | `/backend/customer_totals.php` | — |\n| Ambient Weather | `/backend/ambient_weather.php` | — |\n| Bills Manager | `/backend/bills.php` | — |\n| Stocks & Crypto | `/backend/stocks_crypto.php` | — |\n| Cron Manager | `/backend/cron_manager.php` | — |\n| Error Log | `/backend/error_log.php` | Change Log |\n\n---\n\n## 8. Documents Editor — Full Rewrite (`documents.php`)\n\n### Problem\nGitHub Copilot introduced several bugs into the previous version:\n- `docLoad()` had mismatched closing braces, breaking the entire function\n- The document library was constrained to ~40% page width inside a grid column\n- The editor was a plain `<textarea>` with no formatting support\n\n### Solution\nComplete rewrite of all HTML, CSS, and JavaScript. PHP POST handlers were kept\nidentical — only the front-end was replaced.\n\n### Editor: EasyMDE\nReplaced the plain textarea with **EasyMDE** (Easy Markdown Editor) via CDN:\n```html\n<link rel=\"stylesheet\" href=\"https://unpkg.com/easymde/dist/easymde.min.css\">\n<script src=\"https://unpkg.com/easymde/dist/easymde.min.js\"></script>\n```\n\nEasyMDE wraps the `<textarea id=\"content_markdown\">` and syncs content on form submit.\n`ta.value = easyMDE.value()` is called explicitly in `submitDoc()` before `form.submit()`.\n\n### Toolbar Buttons\nSave (custom gradient btn) | Bold · Italic · Strikethrough | H1 · H2 · H3 |\nUnordered List · Ordered List · Checklist (custom) | Quote · Code · Table |\nHR · Link · Image URL (custom) | Undo · Redo | Fullscreen | Guide\n\n- **Save** button in toolbar calls `submitDoc()`, same as the Save Document button below\n- **Ctrl+S / Cmd+S** keyboard shortcut via CodeMirror `extraKeys`\n- **Checklist** inserts `- [ ] ` prefix on selected text\n- **Image URL** prompts for URL and alt text, inserts `![alt](url)` syntax\n\n### Preview Toggle\nEasyMDE's built-in `preview` and `side-by-side` buttons were **removed** to prevent\nboth raw markdown and rendered HTML showing simultaneously.\n\nA custom **Preview** button in the panel header replaces them:\n- Uses `marked.js` (CDN) to render the markdown client-side\n- **Preview on**: hides `EasyMDEContainer` and highlight strip; shows `#doc-preview-pane`\n- **Edit on**: hides preview pane; shows EasyMDE and highlight strip; calls `codemirror.refresh()`\n- Loading a document via `docLoad()` automatically returns to edit mode\n- Button state: pill shape, switches between \"👁 Preview\" and \"✏ Edit\" labels\n\n```html\n<script src=\"https://cdn.jsdelivr.net/npm/marked/marked.min.js\"></script>\n```\n\n### `docLoad()` — Clean Rewrite\nOld function had mismatched braces and redundant `editor.value` assignments.\nNew function:\n1. Returns to edit mode if preview is on\n2. Sets heading to \"Loading…\"\n3. Highlights active `<tr>` and `<a>` in library immediately\n4. `fetch('/backend/api/documents.php?backend_document_id=' + id)`\n5. Populates all meta fields (type, title, slug, summary)\n6. Sets `easyMDE.value(content)` from `editor_content` or `content_markdown`\n7. Shows/hides file notice\n8. Calls `renderVersionList(data.versions)`\n9. Updates URL via `history.replaceState()`\n10. Flashes panel border with accent color for 800 ms\n\n### Library Panel — Full Width\nOld layout: library was inside a 2-column grid alongside the editor.\nNew layout:\n```\n[Editor Panel          — full width]\n[Library Panel         — full width, max-height: 460px scrollable]\n[Upload | Versions     — 2-column row]\n```\n\nLive client-side search filters table rows by `textContent` without a page reload.\n\n### Word Count & Highlights\n- Word count and char count update on every keystroke via `codemirror.on('change', ...)`\n- 6 highlight color swatches + a clear option wrap selected text in\n  `<span class=\"hl\" style=\"background:COLOR\">...</span>`\n- Clear highlight strips existing `<span class=\"hl\">` wrappers from selected text\n\n### CSS Theme Conformity (visit.html standard)\nAll custom CSS was updated to match the application theme:\n\n| Property | Before | After |\n|----------|--------|-------|\n| Form input border-radius | `10px` | `12px` (matches `--radius-sm`) |\n| Form input padding | `8px 10px` | `11px 14px` |\n| Field label font-size | `0.73rem` + uppercase | `0.82rem`, no forced uppercase |\n| Field label letter-spacing | `0.07em` | `0.04em` |\n| Focus ring | `outline: 2px solid var(--accent)` | `outline: 2px solid rgba(175,77,49,0.3)` |\n| Panel header background | `rgba(244,239,231,0.6)` hardcoded | `var(--panel)` |\n| Toolbar background | `rgba(244,239,231,0.7)` hardcoded | `rgba(255,255,255,0.55)` |\n| Action bar background | `rgba(244,239,231,0.4)` hardcoded | `rgba(255,255,255,0.25)` |\n| Save button | `background: var(--accent)` | `linear-gradient(135deg, var(--accent), var(--accent-deep))` |\n| Action buttons | `border-radius: 7px` | `border-radius: 999px` pill shape |\n| Active library row tint | `rgba(26,122,154,0.06)` teal | `rgba(175,77,49,0.07)` accent |\n| Type badge (markdown) | `rgba(26,122,154,0.1)` teal | `rgba(175,77,49,0.1)` accent |\n| File notice tint | `rgba(26,122,154,0.06)` teal | `rgba(175,77,49,0.05)` accent |\n\n---\n\n## 9. `document_service.php` — Parse Error Fix\n\n### Problem\nThe function `app_backend_documents_dir()` had its closing `return $path; }` accidentally\ndisplaced to after `app_document_can_edit_inline()` (lines 147–148), with the rest of the\nfile's functions (`app_libreoffice_convert_to_html`, etc.) nested inside the unclosed\nfunction body. This caused a PHP parse error on every request to any page that\n`require_once`'s `document_service.php`, making `app_document_can_edit_inline()`\nappear undefined.\n\n### Fix\nTwo edits to `document_service.php`:\n\n1. Closed `app_backend_documents_dir()` properly after the `mkdir` block:\n```php\nfunction app_backend_documents_dir(): string\n{\n    $path = __DIR__ . '/documents_storage';\n    if (!is_dir($path)) {\n        mkdir($path, 0775, true);\n    }\n    return $path;   // ← added\n}                   // ← added\n```\n\n2. Removed the orphaned `return $path; }` that had been displaced to after\n`app_document_can_edit_inline()` at lines 147–148.\n\n---\n\n## Access URLs\n\n| Module | URL |\n|--------|-----|\n| Backend Home | `/backend/` |\n| Bills Manager | `/backend/bills.php` |\n| Stocks & Crypto | `/backend/stocks_crypto.php` |\n| Cron Manager | `/backend/cron_manager.php` |\n| Error Log | `/backend/error_log.php` |\n| Change Log | `/backend/change_log.php` |\n| Documents | `/backend/documents.php` |\n| Documents API | `/backend/api/documents.php` |\n\n---\n\n## Technology Used\n\n| Library | Version | Purpose |\n|---------|---------|---------|\n| EasyMDE | latest (unpkg CDN) | Rich markdown editor |\n| marked.js | latest (jsdelivr CDN) | Client-side markdown → HTML rendering |\n| Yahoo Finance API | v8 (unofficial) | Live stock/ETF price fetching |\n| PHPMailer | (existing) | Email sending via `app_send_mail()` |\n\n---\n\n## Coding Conventions (consistent with existing codebase)\n\n- `declare(strict_types=1)` in all PHP files\n- `app_db()` for database connections, `mysqli` throughout\n- `app_query_all()`, `app_query_one()`, `app_h()`, `app_send_json()` helpers\n- `app_log_backend_exception()` for error logging\n- POST/Redirect/GET for all form submissions\n- `ON DUPLICATE KEY UPDATE` for safe upserts\n- `app_backend_render_header()` / `app_backend_render_footer()` for page chrome\n- CSS custom properties: `--accent`, `--panel`, `--line`, `--text`, `--muted`, `--shadow`\n- All new tables use `CREATE TABLE IF NOT EXISTS` (idempotent, no separate migration needed)\n","is_text_editable":1,"can_edit_inline":1}
After
{"backend_document_id":"10","document_type":"upload","title":"Claude 04-07-2026","slug":"claude-04-07-2026","summary_text":"Claude 04-07-2026 fixing backend Document areas","content_markdown":"# Session Log — 2026-04-07\r\n\r\nAll work completed in this session on the CustomerDB backend application.\r\n\r\n---\r\n\r\n## Summary of New Modules and Changes\r\n\r\n| Area | File(s) | Status |\r\n|------|---------|--------|\r\n| Bills Manager | `backend/bills_service.php`, `backend/bills.php` | New |\r\n| Next-Day Email Job | `backend/jobs/send_nextday_customer_emails.php` | New |\r\n| Cron Manager UI | `backend/cron_manager.php` | New |\r\n| Error Log UI | `backend/error_log.php` | New |\r\n| Change Log UI | `backend/change_log.php` | New |\r\n| Stocks & Crypto | `backend/stocks_crypto_service.php`, `backend/stocks_crypto.php` | New |\r\n| Backend Home | `backend/index.php` | Modified |\r\n| Documents Editor | `backend/documents.php` | Full rewrite |\r\n| Documents Service | `backend/document_service.php` | Bug fix |\r\n\r\n---\r\n\r\n## 1. Bills Manager\r\n\r\n### Files\r\n- `backend/bills_service.php` — 356 lines, service layer\r\n- `backend/bills.php` — 972 lines, UI page\r\n\r\n### Purpose\r\nReplaces a spreadsheet (`newbills2026.xlsx`) with a web-based bill payment tracker.\r\nTracks bank account opening balances, monthly bill payments, and computes running bank\r\nbalances in the same layout as the Excel spreadsheet.\r\n\r\n### Database Tables (auto-created on first load)\r\n\r\n**`bills_account`**\r\n| Column | Type | Notes |\r\n|--------|------|-------|\r\n| `bills_account_id` | INT PK AUTO | |\r\n| `name` | VARCHAR(120) | Bank name |\r\n| `sort_order` | INT | Display order |\r\n\r\n**`bills_item`**\r\n| Column | Type | Notes |\r\n|--------|------|-------|\r\n| `bills_item_id` | INT PK AUTO | |\r\n| `label` | VARCHAR(200) | Bill name |\r\n| `default_amount` | DECIMAL(12,2) | Pre-fills the Due column |\r\n| `default_account_id` | INT | Default bank to debit |\r\n| `notes` | TEXT | Instructions / URLs |\r\n| `sort_order` | INT | Row order |\r\n| `is_deleted` | TINYINT | Soft delete |\r\n\r\n**`bills_entry`**\r\nMonthly bill payment rows.\r\n\r\n| Column | Type | Notes |\r\n|--------|------|-------|\r\n| `bills_entry_id` | INT PK AUTO | |\r\n| `bills_item_id` | INT | FK to `bills_item` |\r\n| `year`, `month` | INT | Billing period |\r\n| `amount_due` | DECIMAL(12,2) | Amount billed |\r\n| `amount_paid` | DECIMAL(12,2) | Amount actually paid |\r\n| `account_id` | INT | Bank used to pay |\r\n\r\n**`bills_balance`**\r\nOpening bank balances per month.\r\n\r\n| Column | Type | Notes |\r\n|--------|------|-------|\r\n| `bills_balance_id` | INT PK AUTO | |\r\n| `account_id` | INT | FK to `bills_account` |\r\n| `year`, `month` | INT | Period |\r\n| `opening_balance` | DECIMAL(14,2) | Balance at start of month |\r\n\r\n### Seed Data\r\nOn first load (empty table), the following are auto-seeded from the original Excel file:\r\n\r\n**Accounts:** USAA, MIDFLORIDA, Centenial, CapitalOne\r\n\r\n**Bills (11 items):** MidFlorida Mortgage, MidFlorida Credit Card, USAA Credit Card,\r\nCentenial Bank, Electricity, Waste Management, Comcast/Xfinity, Insurance, Water,\r\nCapitalOne #1, CapitalOne #2\r\n\r\n### Service Functions\r\n\r\n```php\r\napp_install_bills_schema(mysqli $conn): void\r\napp_bills_seed_defaults(mysqli $conn): void\r\napp_bills_accounts(mysqli $conn): array\r\napp_bills_all_accounts(mysqli $conn): array\r\napp_bills_items(mysqli $conn): array\r\napp_bills_balances(mysqli $conn, int $year, int $month): array       // map: account_id → float\r\napp_bills_entries(mysqli $conn, int $year, int $month): array        // map: item_id → [due, paid, account_id]\r\napp_bills_active_months(mysqli $conn, int $year): array\r\napp_bills_save_balance(mysqli $conn, int $year, int $month, int $accountId, float $balance): void\r\napp_bills_save_entry(mysqli $conn, int $year, int $month, int $itemId, ?float $due, ?float $paid, ?int $accountId): void\r\napp_bills_save_account(mysqli $conn, array $data): int\r\napp_bills_delete_account(mysqli $conn, int $id): void\r\napp_bills_save_item(mysqli $conn, array $data): int\r\napp_bills_delete_item(mysqli $conn, int $id): void\r\napp_bills_reorder_item(mysqli $conn, int $id, string $dir): void\r\napp_bills_copy_month(mysqli $conn, int $fromYear, int $fromMonth, int $toYear, int $toMonth): int\r\n```\r\n\r\n### UI Features (`bills.php`)\r\n- Month/year navigation with ◀ / ▶ buttons and active-month pills\r\n- **Bank Balances section** — editable opening balance per account, inline Save button\r\n- **Bills table** — spreadsheet-style rows with:\r\n  - Amount Due (editable, pre-filled from default)\r\n  - Amount Paid (editable, AJAX auto-save after 600 ms debounce)\r\n  - Account selector (which bank pays this bill)\r\n  - Balance After column (computed client-side in real time)\r\n  - Up/Down sort buttons\r\n  - Edit and Delete per row\r\n- **Sticky running totals sidebar** — shows each bank's current balance as bills are entered\r\n- **Copy from previous month** — modal to copy all entries from a prior month to the current one\r\n- **Add Bill / Edit Bill modals**\r\n- **Add Account / Edit Account modals**\r\n- All saves use POST/Redirect/GET; bill entry changes auto-save via `fetch()` AJAX\r\n\r\n### Running Total Calculation\r\nJavaScript function `recalcTotals()` walks bills in DOM order, maintains a\r\n`bal[accountId]` map starting from opening balances, subtracts `amount_paid` from the\r\nselected account for each bill, and updates the \"Balance After\" column and sidebar panel\r\nin real time — matching the Excel spreadsheet formula logic.\r\n\r\n---\r\n\r\n## 2. Next-Day Customer Email Job\r\n\r\n### File\r\n- `backend/jobs/send_nextday_customer_emails.php` — 158 lines\r\n\r\n### Purpose\r\nPHP CLI translation of the VBA macro `RunNextDayCustomerEmails()`.\r\nQueries `setmore_appointments` for appointments scheduled for tomorrow\r\n(status ≠ Cancelled) and sends a reminder email to each customer.\r\n\r\n### Usage\r\n```bash\r\n# Normal run (sends real emails)\r\nphp backend/jobs/send_nextday_customer_emails.php\r\n\r\n# Dry run — prints what would be sent, no emails sent\r\nphp backend/jobs/send_nextday_customer_emails.php --dry-run\r\n\r\n# Override date (for testing a specific date)\r\nphp backend/jobs/send_nextday_customer_emails.php --date=2026-04-09\r\n```\r\n\r\n### Recommended Cron Schedule\r\n```\r\n0 20 * * *  php /path/to/backend/jobs/send_nextday_customer_emails.php\r\n```\r\nRuns at 8:00 PM every evening, sends reminders for the following day's appointments.\r\n\r\n### Email Content\r\n- Subject: `Reminder: Your appointment at Ella's Alterations tomorrow`\r\n- Body matches the original VBA email with full address, phone, 5-line award\r\n  signature block, and a plain-text fallback\r\n- Uses `app_send_mail()` / PHPMailer\r\n\r\n---\r\n\r\n## 3. Cron Manager UI\r\n\r\n### File\r\n- `backend/cron_manager.php` — 482 lines\r\n\r\n### Purpose\r\nWeb UI for managing scheduled background jobs. Stores job definitions in the database,\r\nallows toggling on/off, running immediately, editing schedules, and exporting to\r\n`/etc/cron.d/` format.\r\n\r\n### Database Table (auto-created)\r\n\r\n**`cron_job`**\r\n| Column | Type | Notes |\r\n|--------|------|-------|\r\n| `cron_job_id` | INT PK AUTO | |\r\n| `label` | VARCHAR(120) | Human name |\r\n| `description` | TEXT | What the job does |\r\n| `schedule` | VARCHAR(100) | Cron expression e.g. `0 20 * * *` |\r\n| `command` | VARCHAR(500) | Shell command to run |\r\n| `enabled` | TINYINT | 0 = disabled, 1 = enabled |\r\n| `last_run_at` | DATETIME | Updated on \"Run Now\" |\r\n| `last_output` | TEXT | Captured stdout/stderr |\r\n| `sort_order` | INT | Display order |\r\n\r\n### Seeded Jobs (on first install)\r\n1. **Next-Day Customer Emails** — `0 20 * * *` — `php backend/jobs/send_nextday_customer_emails.php`\r\n2. **Morning Jobs** — `0 7 * * *` — `php backend/jobs/morning_jobs.php`\r\n3. **Nightly Reports** — `0 23 * * *` — `php backend/jobs/nightly_reports.php`\r\n4. **DB Backup** — `0 2 * * *` — `bash backend/bin/db_backup.sh`\r\n5. **Daily Cleanup** — `30 3 * * *` — `php backend/jobs/daily_cleanup.php`\r\n6. **Add Name Records Auto Delete** — `0 4 * * *` — `php backend/jobs/cleanup_name_records.php`\r\n\r\nNew jobs are added idempotently (checked by label before inserting) so existing\r\ninstallations are not re-seeded.\r\n\r\n### UI Features\r\n- Table listing all jobs with schedule, enabled toggle, last run time\r\n- **Toggle** — flip enabled/disabled immediately (POST/redirect)\r\n- **▶ Run Now** — executes the command via `exec()`, captures output, stores to DB, shows in modal\r\n- **Add / Edit** — form with label, description, command, schedule, enabled flag\r\n- **Schedule presets** — quick-select buttons (hourly, daily at common times, weekly, monthly)\r\n- `cron_describe(string $expr): string` — converts `0 20 * * *` → \"Daily at 20:00\"\r\n- **Export Crontab** — writes `/tmp/customerdb_crontab_export.txt` in `/etc/cron.d/` format\r\n\r\n---\r\n\r\n## 4. Error Log UI\r\n\r\n### File\r\n- `backend/error_log.php` — 262 lines\r\n\r\n### Purpose\r\nDedicated UI for browsing `backend_error_log` records with filtering, pagination,\r\nand per-entry management.\r\n\r\n### Features\r\n- **Stats bar** — Total errors, Today's errors, Latest error timestamp\r\n- **Filter bar** — Search (message/context), Source dropdown, Severity dropdown\r\n- **Pagination** — 50 per page with smart ellipsis navigator\r\n- **Per-row delete** — removes individual log entries\r\n- **Clear All button** — truncates the entire log (with confirmation)\r\n- JSON context blocks displayed in `<details>` collapsibles with dark monospace styling\r\n- URL: `/backend/error_log.php`\r\n\r\n---\r\n\r\n## 5. Change Log UI\r\n\r\n### File\r\n- `backend/change_log.php` — 248 lines\r\n\r\n### Purpose\r\nDedicated UI for browsing `backend_change_log` records — audit trail of all\r\ndata changes across the application.\r\n\r\n### Features\r\n- **Filter bar** — Search, Area, Action type, Entity type dropdowns\r\n- **Pagination** — smart ellipsis navigator\r\n- **Per-row detail panels:**\r\n  - Before / After / Context — collapsed JSON blocks\r\n  - Changed Data — open by default, shows the changed fields summary\r\n- Action badges color-coded by type (create, update, delete)\r\n- URL: `/backend/change_log.php`\r\n\r\n---\r\n\r\n## 6. Stocks & Crypto Module\r\n\r\n### Files\r\n- `backend/stocks_crypto_service.php` — 269 lines, service layer\r\n- `backend/stocks_crypto.php` — 667 lines, UI page\r\n\r\n### Purpose\r\nTrack a personal investment portfolio — individual stocks, ETFs, precious metals,\r\nand crypto. Supports live price fetching, manual daily price entry, monthly crypto\r\nbuy tracking, and portfolio gain/loss analysis.\r\n\r\n### Database Tables (auto-created)\r\n\r\n**`stocks_asset`**\r\n| Column | Type | Notes |\r\n|--------|------|-------|\r\n| `stocks_asset_id` | INT PK AUTO | |\r\n| `ticker` | VARCHAR(20) | e.g. `AAPL`, `BTC`, `GLD` |\r\n| `name` | VARCHAR(120) | Display name |\r\n| `asset_type` | ENUM | `stock`, `etf`, `crypto`, `metal`, `other` |\r\n| `shares` | DECIMAL(18,8) | Quantity owned |\r\n| `cost_basis_total` | DECIMAL(14,6) | Total purchase cost |\r\n| `is_active` | TINYINT | Soft hide |\r\n\r\n**`stocks_price`**\r\nDaily price records.\r\n\r\n| Column | Type | Notes |\r\n|--------|------|-------|\r\n| `stocks_price_id` | INT PK AUTO | |\r\n| `ticker` | VARCHAR(20) | |\r\n| `price_date` | DATE | |\r\n| `price` | DECIMAL(14,6) | |\r\n| `source` | VARCHAR(40) | `manual` or `yahoo` |\r\n\r\nUnique constraint on `(ticker, price_date)` — upserts with `ON DUPLICATE KEY UPDATE`.\r\n\r\n**`crypto_monthly_buy`**\r\n| Column | Type | Notes |\r\n|--------|------|-------|\r\n| `crypto_monthly_buy_id` | INT PK AUTO | |\r\n| `ticker` | VARCHAR(20) | |\r\n| `year`, `month` | INT | |\r\n| `quantity` | DECIMAL(18,8) | Crypto units purchased |\r\n| `amount_usd` | DECIMAL(14,2) | USD spent |\r\n| `notes` | VARCHAR(255) | |\r\n\r\n### Service Functions\r\n\r\n```php\r\napp_install_stocks_schema(mysqli $conn): void\r\napp_stocks_assets(mysqli $conn, string $type = ''): array\r\napp_stocks_save_asset(mysqli $conn, array $data): int\r\napp_stocks_delete_asset(mysqli $conn, int $id): void\r\napp_stocks_save_price(mysqli $conn, string $ticker, string $date, float $price, string $source = 'manual'): void\r\napp_stocks_save_prices_bulk(mysqli $conn, string $date, array $prices): int\r\napp_stocks_latest_prices(mysqli $conn): array                          // map: ticker → [price, date]\r\napp_stocks_price_history(mysqli $conn, string $ticker, int $limit = 90): array\r\napp_stocks_fetch_live_price(string $ticker): ?float                    // Yahoo Finance API, 8s timeout\r\napp_stocks_fetch_all_live(mysqli $conn): array                         // fetches all active tickers\r\napp_crypto_monthly_buys(mysqli $conn, int $year, int $month = 0): array\r\napp_crypto_save_monthly_buy(mysqli $conn, array $data): void\r\napp_crypto_delete_monthly_buy(mysqli $conn, int $id): void\r\napp_stocks_dashboard(mysqli $conn): array                              // cost_basis, current_value, gain_loss, gain_pct per asset\r\napp_stocks_stats(mysqli $conn): array                                  // total portfolio stats\r\n```\r\n\r\n### Live Price Source\r\nYahoo Finance unofficial API:\r\n```\r\nhttps://query1.finance.yahoo.com/v8/finance/chart/{TICKER}?interval=1d&range=1d\r\n```\r\n8-second timeout, reads `.chart.result[0].meta.regularMarketPrice`.\r\n\r\n### UI Tabs (`stocks_crypto.php`)\r\n\r\n1. **Portfolio** — dashboard table with ticker, shares, cost basis, current value,\r\n   gain/loss, gain %, latest price date. Stats bar shows total value, total cost,\r\n   total gain/loss. \"Fetch Live Prices\" button calls Yahoo Finance for all tickers.\r\n\r\n2. **Enter Prices** — manual daily price entry grid for all active assets,\r\n   date picker (defaults to today), bulk save.\r\n\r\n3. **Crypto Buys** — monthly crypto purchase log. Month/year selector, table of\r\n   ticker/quantity/USD/notes rows, add and delete per entry.\r\n\r\n4. **Add Asset** — form to add a new stock, ETF, crypto, metal, or other asset.\r\n\r\n### Auto-Refresh\r\nA checkbox above the stats bar auto-refreshes the Portfolio tab every 10 seconds.\r\nState persists in `localStorage` key `sc_auto_refresh`. On reload, strips `?ok=`\r\nand `?warn=` query params to prevent the notice from repeating every 10 seconds.\r\n\r\n---\r\n\r\n## 7. Backend Home — New Cards (`index.php`)\r\n\r\nSix new cards added to the backend home dashboard:\r\n\r\n| Card | Link | Secondary Action |\r\n|------|------|-----------------|\r\n| Customer Totals | `/backend/customer_totals.php` | — |\r\n| Ambient Weather | `/backend/ambient_weather.php` | — |\r\n| Bills Manager | `/backend/bills.php` | — |\r\n| Stocks & Crypto | `/backend/stocks_crypto.php` | — |\r\n| Cron Manager | `/backend/cron_manager.php` | — |\r\n| Error Log | `/backend/error_log.php` | Change Log |\r\n\r\n---\r\n\r\n## 8. Documents Editor — Full Rewrite (`documents.php`)\r\n\r\n### Problem\r\nGitHub Copilot introduced several bugs into the previous version:\r\n- `docLoad()` had mismatched closing braces, breaking the entire function\r\n- The document library was constrained to ~40% page width inside a grid column\r\n- The editor was a plain `<textarea>` with no formatting support\r\n\r\n### Solution\r\nComplete rewrite of all HTML, CSS, and JavaScript. PHP POST handlers were kept\r\nidentical — only the front-end was replaced.\r\n\r\n### Editor: EasyMDE\r\nReplaced the plain textarea with **EasyMDE** (Easy Markdown Editor) via CDN:\r\n```html\r\n<link rel=\"stylesheet\" href=\"https://unpkg.com/easymde/dist/easymde.min.css\">\r\n<script src=\"https://unpkg.com/easymde/dist/easymde.min.js\"></script>\r\n```\r\n\r\nEasyMDE wraps the `<textarea id=\"content_markdown\">` and syncs content on form submit.\r\n`ta.value = easyMDE.value()` is called explicitly in `submitDoc()` before `form.submit()`.\r\n\r\n### Toolbar Buttons\r\nSave (custom gradient btn) | Bold · Italic · Strikethrough | H1 · H2 · H3 |\r\nUnordered List · Ordered List · Checklist (custom) | Quote · Code · Table |\r\nHR · Link · Image URL (custom) | Undo · Redo | Fullscreen | Guide\r\n\r\n- **Save** button in toolbar calls `submitDoc()`, same as the Save Document button below\r\n- **Ctrl+S / Cmd+S** keyboard shortcut via CodeMirror `extraKeys`\r\n- **Checklist** inserts `- [ ] ` prefix on selected text\r\n- **Image URL** prompts for URL and alt text, inserts `![alt](url)` syntax\r\n\r\n### Preview Toggle\r\nEasyMDE's built-in `preview` and `side-by-side` buttons were **removed** to prevent\r\nboth raw markdown and rendered HTML showing simultaneously.\r\n\r\nA custom **Preview** button in the panel header replaces them:\r\n- Uses `marked.js` (CDN) to render the markdown client-side\r\n- **Preview on**: hides `EasyMDEContainer` and highlight strip; shows `#doc-preview-pane`\r\n- **Edit on**: hides preview pane; shows EasyMDE and highlight strip; calls `codemirror.refresh()`\r\n- Loading a document via `docLoad()` automatically returns to edit mode\r\n- Button state: pill shape, switches between \"👁 Preview\" and \"✏ Edit\" labels\r\n\r\n```html\r\n<script src=\"https://cdn.jsdelivr.net/npm/marked/marked.min.js\"></script>\r\n```\r\n\r\n### `docLoad()` — Clean Rewrite\r\nOld function had mismatched braces and redundant `editor.value` assignments.\r\nNew function:\r\n1. Returns to edit mode if preview is on\r\n2. Sets heading to \"Loading…\"\r\n3. Highlights active `<tr>` and `<a>` in library immediately\r\n4. `fetch('/backend/api/documents.php?backend_document_id=' + id)`\r\n5. Populates all meta fields (type, title, slug, summary)\r\n6. Sets `easyMDE.value(content)` from `editor_content` or `content_markdown`\r\n7. Shows/hides file notice\r\n8. Calls `renderVersionList(data.versions)`\r\n9. Updates URL via `history.replaceState()`\r\n10. Flashes panel border with accent color for 800 ms\r\n\r\n### Library Panel — Full Width\r\nOld layout: library was inside a 2-column grid alongside the editor.\r\nNew layout:\r\n```\r\n[Editor Panel          — full width]\r\n[Library Panel         — full width, max-height: 460px scrollable]\r\n[Upload | Versions     — 2-column row]\r\n```\r\n\r\nLive client-side search filters table rows by `textContent` without a page reload.\r\n\r\n### Word Count & Highlights\r\n- Word count and char count update on every keystroke via `codemirror.on('change', ...)`\r\n- 6 highlight color swatches + a clear option wrap selected text in\r\n  `<span class=\"hl\" style=\"background:COLOR\">...</span>`\r\n- Clear highlight strips existing `<span class=\"hl\">` wrappers from selected text\r\n\r\n### CSS Theme Conformity (visit.html standard)\r\nAll custom CSS was updated to match the application theme:\r\n\r\n| Property | Before | After |\r\n|----------|--------|-------|\r\n| Form input border-radius | `10px` | `12px` (matches `--radius-sm`) |\r\n| Form input padding | `8px 10px` | `11px 14px` |\r\n| Field label font-size | `0.73rem` + uppercase | `0.82rem`, no forced uppercase |\r\n| Field label letter-spacing | `0.07em` | `0.04em` |\r\n| Focus ring | `outline: 2px solid var(--accent)` | `outline: 2px solid rgba(175,77,49,0.3)` |\r\n| Panel header background | `rgba(244,239,231,0.6)` hardcoded | `var(--panel)` |\r\n| Toolbar background | `rgba(244,239,231,0.7)` hardcoded | `rgba(255,255,255,0.55)` |\r\n| Action bar background | `rgba(244,239,231,0.4)` hardcoded | `rgba(255,255,255,0.25)` |\r\n| Save button | `background: var(--accent)` | `linear-gradient(135deg, var(--accent), var(--accent-deep))` |\r\n| Action buttons | `border-radius: 7px` | `border-radius: 999px` pill shape |\r\n| Active library row tint | `rgba(26,122,154,0.06)` teal | `rgba(175,77,49,0.07)` accent |\r\n| Type badge (markdown) | `rgba(26,122,154,0.1)` teal | `rgba(175,77,49,0.1)` accent |\r\n| File notice tint | `rgba(26,122,154,0.06)` teal | `rgba(175,77,49,0.05)` accent |\r\n\r\n---\r\n\r\n## 9. `document_service.php` — Parse Error Fix\r\n\r\n### Problem\r\nThe function `app_backend_documents_dir()` had its closing `return $path; }` accidentally\r\ndisplaced to after `app_document_can_edit_inline()` (lines 147–148), with the rest of the\r\nfile's functions (`app_libreoffice_convert_to_html`, etc.) nested inside the unclosed\r\nfunction body. This caused a PHP parse error on every request to any page that\r\n`require_once`'s `document_service.php`, making `app_document_can_edit_inline()`\r\nappear undefined.\r\n\r\n### Fix\r\nTwo edits to `document_service.php`:\r\n\r\n1. Closed `app_backend_documents_dir()` properly after the `mkdir` block:\r\n```php\r\nfunction app_backend_documents_dir(): string\r\n{\r\n    $path = __DIR__ . '/documents_storage';\r\n    if (!is_dir($path)) {\r\n        mkdir($path, 0775, true);\r\n    }\r\n    return $path;   // ← added\r\n}                   // ← added\r\n```\r\n\r\n2. Removed the orphaned `return $path; }` that had been displaced to after\r\n`app_document_can_edit_inline()` at lines 147–148.\r\n\r\n---\r\n\r\n## Access URLs\r\n\r\n| Module | URL |\r\n|--------|-----|\r\n| Backend Home | `/backend/` |\r\n| Bills Manager | `/backend/bills.php` |\r\n| Stocks & Crypto | `/backend/stocks_crypto.php` |\r\n| Cron Manager | `/backend/cron_manager.php` |\r\n| Error Log | `/backend/error_log.php` |\r\n| Change Log | `/backend/change_log.php` |\r\n| Documents | `/backend/documents.php` |\r\n| Documents API | `/backend/api/documents.php` |\r\n\r\n---\r\n\r\n## Technology Used\r\n\r\n| Library | Version | Purpose |\r\n|---------|---------|---------|\r\n| EasyMDE | latest (unpkg CDN) | Rich markdown editor |\r\n| marked.js | latest (jsdelivr CDN) | Client-side markdown → HTML rendering |\r\n| Yahoo Finance API | v8 (unofficial) | Live stock/ETF price fetching |\r\n| PHPMailer | (existing) | Email sending via `app_send_mail()` |\r\n\r\n---\r\n\r\n## Coding Conventions (consistent with existing codebase)\r\n\r\n- `declare(strict_types=1)` in all PHP files\r\n- `app_db()` for database connections, `mysqli` throughout\r\n- `app_query_all()`, `app_query_one()`, `app_h()`, `app_send_json()` helpers\r\n- `app_log_backend_exception()` for error logging\r\n- POST/Redirect/GET for all form submissions\r\n- `ON DUPLICATE KEY UPDATE` for safe upserts\r\n- `app_backend_render_header()` / `app_backend_render_footer()` for page chrome\r\n- CSS custom properties: `--accent`, `--panel`, `--line`, `--text`, `--muted`, `--shadow`\r\n- All new tables use `CREATE TABLE IF NOT EXISTS` (idempotent, no separate migration needed)","content_html":"<h1>Session Log — 2026-04-07</h1>\n<p>All work completed in this session on the CustomerDB backend application.</p>\n<p>---</p>\n<h2>Summary of New Modules and Changes</h2>\n<p>| Area | File(s) | Status |</p>\n<p>|------|---------|--------|</p>\n<p>| Bills Manager | `backend/bills_service.php`, `backend/bills.php` | New |</p>\n<p>| Next-Day Email Job | `backend/jobs/send_nextday_customer_emails.php` | New |</p>\n<p>| Cron Manager UI | `backend/cron_manager.php` | New |</p>\n<p>| Error Log UI | `backend/error_log.php` | New |</p>\n<p>| Change Log UI | `backend/change_log.php` | New |</p>\n<p>| Stocks &amp; Crypto | `backend/stocks_crypto_service.php`, `backend/stocks_crypto.php` | New |</p>\n<p>| Backend Home | `backend/index.php` | Modified |</p>\n<p>| Documents Editor | `backend/documents.php` | Full rewrite |</p>\n<p>| Documents Service | `backend/document_service.php` | Bug fix |</p>\n<p>---</p>\n<h2>1. Bills Manager</h2>\n<h3>Files</h3>\n<ul>\n<li>`backend/bills_service.php` — 356 lines, service layer</li>\n<li>`backend/bills.php` — 972 lines, UI page</li>\n</ul>\n<h3>Purpose</h3>\n<p>Replaces a spreadsheet (`newbills2026.xlsx`) with a web-based bill payment tracker.</p>\n<p>Tracks bank account opening balances, monthly bill payments, and computes running bank</p>\n<p>balances in the same layout as the Excel spreadsheet.</p>\n<h3>Database Tables (auto-created on first load)</h3>\n<p><strong>`bills_account`</strong></p>\n<p>| Column | Type | Notes |</p>\n<p>|--------|------|-------|</p>\n<p>| `bills_account_id` | INT PK AUTO | |</p>\n<p>| `name` | VARCHAR(120) | Bank name |</p>\n<p>| `sort_order` | INT | Display order |</p>\n<p><strong>`bills_item`</strong></p>\n<p>| Column | Type | Notes |</p>\n<p>|--------|------|-------|</p>\n<p>| `bills_item_id` | INT PK AUTO | |</p>\n<p>| `label` | VARCHAR(200) | Bill name |</p>\n<p>| `default_amount` | DECIMAL(12,2) | Pre-fills the Due column |</p>\n<p>| `default_account_id` | INT | Default bank to debit |</p>\n<p>| `notes` | TEXT | Instructions / URLs |</p>\n<p>| `sort_order` | INT | Row order |</p>\n<p>| `is_deleted` | TINYINT | Soft delete |</p>\n<p><strong>`bills_entry`</strong></p>\n<p>Monthly bill payment rows.</p>\n<p>| Column | Type | Notes |</p>\n<p>|--------|------|-------|</p>\n<p>| `bills_entry_id` | INT PK AUTO | |</p>\n<p>| `bills_item_id` | INT | FK to `bills_item` |</p>\n<p>| `year`, `month` | INT | Billing period |</p>\n<p>| `amount_due` | DECIMAL(12,2) | Amount billed |</p>\n<p>| `amount_paid` | DECIMAL(12,2) | Amount actually paid |</p>\n<p>| `account_id` | INT | Bank used to pay |</p>\n<p><strong>`bills_balance`</strong></p>\n<p>Opening bank balances per month.</p>\n<p>| Column | Type | Notes |</p>\n<p>|--------|------|-------|</p>\n<p>| `bills_balance_id` | INT PK AUTO | |</p>\n<p>| `account_id` | INT | FK to `bills_account` |</p>\n<p>| `year`, `month` | INT | Period |</p>\n<p>| `opening_balance` | DECIMAL(14,2) | Balance at start of month |</p>\n<h3>Seed Data</h3>\n<p>On first load (empty table), the following are auto-seeded from the original Excel file:</p>\n<p><strong>Accounts:</strong> USAA, MIDFLORIDA, Centenial, CapitalOne</p>\n<p><strong>Bills (11 items):</strong> MidFlorida Mortgage, MidFlorida Credit Card, USAA Credit Card,</p>\n<p>Centenial Bank, Electricity, Waste Management, Comcast/Xfinity, Insurance, Water,</p>\n<p>CapitalOne #1, CapitalOne #2</p>\n<h3>Service Functions</h3>\n<p>```php</p>\n<p>app_install_bills_schema(mysqli $conn): void</p>\n<p>app_bills_seed_defaults(mysqli $conn): void</p>\n<p>app_bills_accounts(mysqli $conn): array</p>\n<p>app_bills_all_accounts(mysqli $conn): array</p>\n<p>app_bills_items(mysqli $conn): array</p>\n<p>app_bills_balances(mysqli $conn, int $year, int $month): array       // map: account_id → float</p>\n<p>app_bills_entries(mysqli $conn, int $year, int $month): array        // map: item_id → [due, paid, account_id]</p>\n<p>app_bills_active_months(mysqli $conn, int $year): array</p>\n<p>app_bills_save_balance(mysqli $conn, int $year, int $month, int $accountId, float $balance): void</p>\n<p>app_bills_save_entry(mysqli $conn, int $year, int $month, int $itemId, ?float $due, ?float $paid, ?int $accountId): void</p>\n<p>app_bills_save_account(mysqli $conn, array $data): int</p>\n<p>app_bills_delete_account(mysqli $conn, int $id): void</p>\n<p>app_bills_save_item(mysqli $conn, array $data): int</p>\n<p>app_bills_delete_item(mysqli $conn, int $id): void</p>\n<p>app_bills_reorder_item(mysqli $conn, int $id, string $dir): void</p>\n<p>app_bills_copy_month(mysqli $conn, int $fromYear, int $fromMonth, int $toYear, int $toMonth): int</p>\n<p>```</p>\n<h3>UI Features (`bills.php`)</h3>\n<ul>\n<li>Month/year navigation with ◀ / ▶ buttons and active-month pills</li>\n<li><strong>Bank Balances section</strong> — editable opening balance per account, inline Save button</li>\n<li><strong>Bills table</strong> — spreadsheet-style rows with:</li>\n<li>Amount Due (editable, pre-filled from default)</li>\n<li>Amount Paid (editable, AJAX auto-save after 600 ms debounce)</li>\n<li>Account selector (which bank pays this bill)</li>\n<li>Balance After column (computed client-side in real time)</li>\n<li>Up/Down sort buttons</li>\n<li>Edit and Delete per row</li>\n<li><strong>Sticky running totals sidebar</strong> — shows each bank&#039;s current balance as bills are entered</li>\n<li><strong>Copy from previous month</strong> — modal to copy all entries from a prior month to the current one</li>\n<li><strong>Add Bill / Edit Bill modals</strong></li>\n<li><strong>Add Account / Edit Account modals</strong></li>\n<li>All saves use POST/Redirect/GET; bill entry changes auto-save via `fetch()` AJAX</li>\n</ul>\n<h3>Running Total Calculation</h3>\n<p>JavaScript function `recalcTotals()` walks bills in DOM order, maintains a</p>\n<p>`bal[accountId]` map starting from opening balances, subtracts `amount_paid` from the</p>\n<p>selected account for each bill, and updates the &quot;Balance After&quot; column and sidebar panel</p>\n<p>in real time — matching the Excel spreadsheet formula logic.</p>\n<p>---</p>\n<h2>2. Next-Day Customer Email Job</h2>\n<h3>File</h3>\n<ul>\n<li>`backend/jobs/send_nextday_customer_emails.php` — 158 lines</li>\n</ul>\n<h3>Purpose</h3>\n<p>PHP CLI translation of the VBA macro `RunNextDayCustomerEmails()`.</p>\n<p>Queries `setmore_appointments` for appointments scheduled for tomorrow</p>\n<p>(status ≠ Cancelled) and sends a reminder email to each customer.</p>\n<h3>Usage</h3>\n<p>```bash</p>\n<h1>Normal run (sends real emails)</h1>\n<p>php backend/jobs/send_nextday_customer_emails.php</p>\n<h1>Dry run — prints what would be sent, no emails sent</h1>\n<p>php backend/jobs/send_nextday_customer_emails.php --dry-run</p>\n<h1>Override date (for testing a specific date)</h1>\n<p>php backend/jobs/send_nextday_customer_emails.php --date=2026-04-09</p>\n<p>```</p>\n<h3>Recommended Cron Schedule</h3>\n<p>```</p>\n<p>0 20 <em> </em> *  php /path/to/backend/jobs/send_nextday_customer_emails.php</p>\n<p>```</p>\n<p>Runs at 8:00 PM every evening, sends reminders for the following day&#039;s appointments.</p>\n<h3>Email Content</h3>\n<ul>\n<li>Subject: `Reminder: Your appointment at Ella&#039;s Alterations tomorrow`</li>\n<li>Body matches the original VBA email with full address, phone, 5-line award</li>\n</ul>\n<p>signature block, and a plain-text fallback</p>\n<ul>\n<li>Uses `app_send_mail()` / PHPMailer</li>\n</ul>\n<p>---</p>\n<h2>3. Cron Manager UI</h2>\n<h3>File</h3>\n<ul>\n<li>`backend/cron_manager.php` — 482 lines</li>\n</ul>\n<h3>Purpose</h3>\n<p>Web UI for managing scheduled background jobs. Stores job definitions in the database,</p>\n<p>allows toggling on/off, running immediately, editing schedules, and exporting to</p>\n<p>`/etc/cron.d/` format.</p>\n<h3>Database Table (auto-created)</h3>\n<p><strong>`cron_job`</strong></p>\n<p>| Column | Type | Notes |</p>\n<p>|--------|------|-------|</p>\n<p>| `cron_job_id` | INT PK AUTO | |</p>\n<p>| `label` | VARCHAR(120) | Human name |</p>\n<p>| `description` | TEXT | What the job does |</p>\n<p>| `schedule` | VARCHAR(100) | Cron expression e.g. `0 20 <em> </em> *` |</p>\n<p>| `command` | VARCHAR(500) | Shell command to run |</p>\n<p>| `enabled` | TINYINT | 0 = disabled, 1 = enabled |</p>\n<p>| `last_run_at` | DATETIME | Updated on &quot;Run Now&quot; |</p>\n<p>| `last_output` | TEXT | Captured stdout/stderr |</p>\n<p>| `sort_order` | INT | Display order |</p>\n<h3>Seeded Jobs (on first install)</h3>\n<p>1. <strong>Next-Day Customer Emails</strong> — `0 20 <em> </em> *` — `php backend/jobs/send_nextday_customer_emails.php`</p>\n<p>2. <strong>Morning Jobs</strong> — `0 7 <em> </em> *` — `php backend/jobs/morning_jobs.php`</p>\n<p>3. <strong>Nightly Reports</strong> — `0 23 <em> </em> *` — `php backend/jobs/nightly_reports.php`</p>\n<p>4. <strong>DB Backup</strong> — `0 2 <em> </em> *` — `bash backend/bin/db_backup.sh`</p>\n<p>5. <strong>Daily Cleanup</strong> — `30 3 <em> </em> *` — `php backend/jobs/daily_cleanup.php`</p>\n<p>6. <strong>Add Name Records Auto Delete</strong> — `0 4 <em> </em> *` — `php backend/jobs/cleanup_name_records.php`</p>\n<p>New jobs are added idempotently (checked by label before inserting) so existing</p>\n<p>installations are not re-seeded.</p>\n<h3>UI Features</h3>\n<ul>\n<li>Table listing all jobs with schedule, enabled toggle, last run time</li>\n<li><strong>Toggle</strong> — flip enabled/disabled immediately (POST/redirect)</li>\n<li><strong>▶ Run Now</strong> — executes the command via `exec()`, captures output, stores to DB, shows in modal</li>\n<li><strong>Add / Edit</strong> — form with label, description, command, schedule, enabled flag</li>\n<li><strong>Schedule presets</strong> — quick-select buttons (hourly, daily at common times, weekly, monthly)</li>\n<li>`cron_describe(string $expr): string` — converts `0 20 <em> </em> *` → &quot;Daily at 20:00&quot;</li>\n<li><strong>Export Crontab</strong> — writes `/tmp/customerdb_crontab_export.txt` in `/etc/cron.d/` format</li>\n</ul>\n<p>---</p>\n<h2>4. Error Log UI</h2>\n<h3>File</h3>\n<ul>\n<li>`backend/error_log.php` — 262 lines</li>\n</ul>\n<h3>Purpose</h3>\n<p>Dedicated UI for browsing `backend_error_log` records with filtering, pagination,</p>\n<p>and per-entry management.</p>\n<h3>Features</h3>\n<ul>\n<li><strong>Stats bar</strong> — Total errors, Today&#039;s errors, Latest error timestamp</li>\n<li><strong>Filter bar</strong> — Search (message/context), Source dropdown, Severity dropdown</li>\n<li><strong>Pagination</strong> — 50 per page with smart ellipsis navigator</li>\n<li><strong>Per-row delete</strong> — removes individual log entries</li>\n<li><strong>Clear All button</strong> — truncates the entire log (with confirmation)</li>\n<li>JSON context blocks displayed in `&lt;details&gt;` collapsibles with dark monospace styling</li>\n<li>URL: `/backend/error_log.php`</li>\n</ul>\n<p>---</p>\n<h2>5. Change Log UI</h2>\n<h3>File</h3>\n<ul>\n<li>`backend/change_log.php` — 248 lines</li>\n</ul>\n<h3>Purpose</h3>\n<p>Dedicated UI for browsing `backend_change_log` records — audit trail of all</p>\n<p>data changes across the application.</p>\n<h3>Features</h3>\n<ul>\n<li><strong>Filter bar</strong> — Search, Area, Action type, Entity type dropdowns</li>\n<li><strong>Pagination</strong> — smart ellipsis navigator</li>\n<li><strong>Per-row detail panels:</strong></li>\n<li>Before / After / Context — collapsed JSON blocks</li>\n<li>Changed Data — open by default, shows the changed fields summary</li>\n<li>Action badges color-coded by type (create, update, delete)</li>\n<li>URL: `/backend/change_log.php`</li>\n</ul>\n<p>---</p>\n<h2>6. Stocks &amp; Crypto Module</h2>\n<h3>Files</h3>\n<ul>\n<li>`backend/stocks_crypto_service.php` — 269 lines, service layer</li>\n<li>`backend/stocks_crypto.php` — 667 lines, UI page</li>\n</ul>\n<h3>Purpose</h3>\n<p>Track a personal investment portfolio — individual stocks, ETFs, precious metals,</p>\n<p>and crypto. Supports live price fetching, manual daily price entry, monthly crypto</p>\n<p>buy tracking, and portfolio gain/loss analysis.</p>\n<h3>Database Tables (auto-created)</h3>\n<p><strong>`stocks_asset`</strong></p>\n<p>| Column | Type | Notes |</p>\n<p>|--------|------|-------|</p>\n<p>| `stocks_asset_id` | INT PK AUTO | |</p>\n<p>| `ticker` | VARCHAR(20) | e.g. `AAPL`, `BTC`, `GLD` |</p>\n<p>| `name` | VARCHAR(120) | Display name |</p>\n<p>| `asset_type` | ENUM | `stock`, `etf`, `crypto`, `metal`, `other` |</p>\n<p>| `shares` | DECIMAL(18,8) | Quantity owned |</p>\n<p>| `cost_basis_total` | DECIMAL(14,6) | Total purchase cost |</p>\n<p>| `is_active` | TINYINT | Soft hide |</p>\n<p><strong>`stocks_price`</strong></p>\n<p>Daily price records.</p>\n<p>| Column | Type | Notes |</p>\n<p>|--------|------|-------|</p>\n<p>| `stocks_price_id` | INT PK AUTO | |</p>\n<p>| `ticker` | VARCHAR(20) | |</p>\n<p>| `price_date` | DATE | |</p>\n<p>| `price` | DECIMAL(14,6) | |</p>\n<p>| `source` | VARCHAR(40) | `manual` or `yahoo` |</p>\n<p>Unique constraint on `(ticker, price_date)` — upserts with `ON DUPLICATE KEY UPDATE`.</p>\n<p><strong>`crypto_monthly_buy`</strong></p>\n<p>| Column | Type | Notes |</p>\n<p>|--------|------|-------|</p>\n<p>| `crypto_monthly_buy_id` | INT PK AUTO | |</p>\n<p>| `ticker` | VARCHAR(20) | |</p>\n<p>| `year`, `month` | INT | |</p>\n<p>| `quantity` | DECIMAL(18,8) | Crypto units purchased |</p>\n<p>| `amount_usd` | DECIMAL(14,2) | USD spent |</p>\n<p>| `notes` | VARCHAR(255) | |</p>\n<h3>Service Functions</h3>\n<p>```php</p>\n<p>app_install_stocks_schema(mysqli $conn): void</p>\n<p>app_stocks_assets(mysqli $conn, string $type = &#039;&#039;): array</p>\n<p>app_stocks_save_asset(mysqli $conn, array $data): int</p>\n<p>app_stocks_delete_asset(mysqli $conn, int $id): void</p>\n<p>app_stocks_save_price(mysqli $conn, string $ticker, string $date, float $price, string $source = &#039;manual&#039;): void</p>\n<p>app_stocks_save_prices_bulk(mysqli $conn, string $date, array $prices): int</p>\n<p>app_stocks_latest_prices(mysqli $conn): array                          // map: ticker → [price, date]</p>\n<p>app_stocks_price_history(mysqli $conn, string $ticker, int $limit = 90): array</p>\n<p>app_stocks_fetch_live_price(string $ticker): ?float                    // Yahoo Finance API, 8s timeout</p>\n<p>app_stocks_fetch_all_live(mysqli $conn): array                         // fetches all active tickers</p>\n<p>app_crypto_monthly_buys(mysqli $conn, int $year, int $month = 0): array</p>\n<p>app_crypto_save_monthly_buy(mysqli $conn, array $data): void</p>\n<p>app_crypto_delete_monthly_buy(mysqli $conn, int $id): void</p>\n<p>app_stocks_dashboard(mysqli $conn): array                              // cost_basis, current_value, gain_loss, gain_pct per asset</p>\n<p>app_stocks_stats(mysqli $conn): array                                  // total portfolio stats</p>\n<p>```</p>\n<h3>Live Price Source</h3>\n<p>Yahoo Finance unofficial API:</p>\n<p>```</p>\n<p>https://query1.finance.yahoo.com/v8/finance/chart/{TICKER}?interval=1d&amp;range=1d</p>\n<p>```</p>\n<p>8-second timeout, reads `.chart.result[0].meta.regularMarketPrice`.</p>\n<h3>UI Tabs (`stocks_crypto.php`)</h3>\n<p>1. <strong>Portfolio</strong> — dashboard table with ticker, shares, cost basis, current value,</p>\n<p>gain/loss, gain %, latest price date. Stats bar shows total value, total cost,</p>\n<p>total gain/loss. &quot;Fetch Live Prices&quot; button calls Yahoo Finance for all tickers.</p>\n<p>2. <strong>Enter Prices</strong> — manual daily price entry grid for all active assets,</p>\n<p>date picker (defaults to today), bulk save.</p>\n<p>3. <strong>Crypto Buys</strong> — monthly crypto purchase log. Month/year selector, table of</p>\n<p>ticker/quantity/USD/notes rows, add and delete per entry.</p>\n<p>4. <strong>Add Asset</strong> — form to add a new stock, ETF, crypto, metal, or other asset.</p>\n<h3>Auto-Refresh</h3>\n<p>A checkbox above the stats bar auto-refreshes the Portfolio tab every 10 seconds.</p>\n<p>State persists in `localStorage` key `sc_auto_refresh`. On reload, strips `?ok=`</p>\n<p>and `?warn=` query params to prevent the notice from repeating every 10 seconds.</p>\n<p>---</p>\n<h2>7. Backend Home — New Cards (`index.php`)</h2>\n<p>Six new cards added to the backend home dashboard:</p>\n<p>| Card | Link | Secondary Action |</p>\n<p>|------|------|-----------------|</p>\n<p>| Customer Totals | `/backend/customer_totals.php` | — |</p>\n<p>| Ambient Weather | `/backend/ambient_weather.php` | — |</p>\n<p>| Bills Manager | `/backend/bills.php` | — |</p>\n<p>| Stocks &amp; Crypto | `/backend/stocks_crypto.php` | — |</p>\n<p>| Cron Manager | `/backend/cron_manager.php` | — |</p>\n<p>| Error Log | `/backend/error_log.php` | Change Log |</p>\n<p>---</p>\n<h2>8. Documents Editor — Full Rewrite (`documents.php`)</h2>\n<h3>Problem</h3>\n<p>GitHub Copilot introduced several bugs into the previous version:</p>\n<ul>\n<li>`docLoad()` had mismatched closing braces, breaking the entire function</li>\n<li>The document library was constrained to ~40% page width inside a grid column</li>\n<li>The editor was a plain `&lt;textarea&gt;` with no formatting support</li>\n</ul>\n<h3>Solution</h3>\n<p>Complete rewrite of all HTML, CSS, and JavaScript. PHP POST handlers were kept</p>\n<p>identical — only the front-end was replaced.</p>\n<h3>Editor: EasyMDE</h3>\n<p>Replaced the plain textarea with <strong>EasyMDE</strong> (Easy Markdown Editor) via CDN:</p>\n<p>```html</p>\n<p>&lt;link rel=&quot;stylesheet&quot; href=&quot;https://unpkg.com/easymde/dist/easymde.min.css&quot;&gt;</p>\n<p>&lt;script src=&quot;https://unpkg.com/easymde/dist/easymde.min.js&quot;&gt;&lt;/script&gt;</p>\n<p>```</p>\n<p>EasyMDE wraps the `&lt;textarea id=&quot;content_markdown&quot;&gt;` and syncs content on form submit.</p>\n<p>`ta.value = easyMDE.value()` is called explicitly in `submitDoc()` before `form.submit()`.</p>\n<h3>Toolbar Buttons</h3>\n<p>Save (custom gradient btn) | Bold · Italic · Strikethrough | H1 · H2 · H3 |</p>\n<p>Unordered List · Ordered List · Checklist (custom) | Quote · Code · Table |</p>\n<p>HR · Link · Image URL (custom) | Undo · Redo | Fullscreen | Guide</p>\n<ul>\n<li><strong>Save</strong> button in toolbar calls `submitDoc()`, same as the Save Document button below</li>\n<li><strong>Ctrl+S / Cmd+S</strong> keyboard shortcut via CodeMirror `extraKeys`</li>\n<li><strong>Checklist</strong> inserts `- [ ] ` prefix on selected text</li>\n<li><strong>Image URL</strong> prompts for URL and alt text, inserts `![alt](url)` syntax</li>\n</ul>\n<h3>Preview Toggle</h3>\n<p>EasyMDE&#039;s built-in `preview` and `side-by-side` buttons were <strong>removed</strong> to prevent</p>\n<p>both raw markdown and rendered HTML showing simultaneously.</p>\n<p>A custom <strong>Preview</strong> button in the panel header replaces them:</p>\n<ul>\n<li>Uses `marked.js` (CDN) to render the markdown client-side</li>\n<li><strong>Preview on</strong>: hides `EasyMDEContainer` and highlight strip; shows `#doc-preview-pane`</li>\n<li><strong>Edit on</strong>: hides preview pane; shows EasyMDE and highlight strip; calls `codemirror.refresh()`</li>\n<li>Loading a document via `docLoad()` automatically returns to edit mode</li>\n<li>Button state: pill shape, switches between &quot;👁 Preview&quot; and &quot;✏ Edit&quot; labels</li>\n</ul>\n<p>```html</p>\n<p>&lt;script src=&quot;https://cdn.jsdelivr.net/npm/marked/marked.min.js&quot;&gt;&lt;/script&gt;</p>\n<p>```</p>\n<h3>`docLoad()` — Clean Rewrite</h3>\n<p>Old function had mismatched braces and redundant `editor.value` assignments.</p>\n<p>New function:</p>\n<p>1. Returns to edit mode if preview is on</p>\n<p>2. Sets heading to &quot;Loading…&quot;</p>\n<p>3. Highlights active `&lt;tr&gt;` and `&lt;a&gt;` in library immediately</p>\n<p>4. `fetch(&#039;/backend/api/documents.php?backend_document_id=&#039; + id)`</p>\n<p>5. Populates all meta fields (type, title, slug, summary)</p>\n<p>6. Sets `easyMDE.value(content)` from `editor_content` or `content_markdown`</p>\n<p>7. Shows/hides file notice</p>\n<p>8. Calls `renderVersionList(data.versions)`</p>\n<p>9. Updates URL via `history.replaceState()`</p>\n<p>10. Flashes panel border with accent color for 800 ms</p>\n<h3>Library Panel — Full Width</h3>\n<p>Old layout: library was inside a 2-column grid alongside the editor.</p>\n<p>New layout:</p>\n<p>```</p>\n<p>[Editor Panel          — full width]</p>\n<p>[Library Panel         — full width, max-height: 460px scrollable]</p>\n<p>[Upload | Versions     — 2-column row]</p>\n<p>```</p>\n<p>Live client-side search filters table rows by `textContent` without a page reload.</p>\n<h3>Word Count &amp; Highlights</h3>\n<ul>\n<li>Word count and char count update on every keystroke via `codemirror.on(&#039;change&#039;, ...)`</li>\n<li>6 highlight color swatches + a clear option wrap selected text in</li>\n</ul>\n<p>`&lt;span class=&quot;hl&quot; style=&quot;background:COLOR&quot;&gt;...&lt;/span&gt;`</p>\n<ul>\n<li>Clear highlight strips existing `&lt;span class=&quot;hl&quot;&gt;` wrappers from selected text</li>\n</ul>\n<h3>CSS Theme Conformity (visit.html standard)</h3>\n<p>All custom CSS was updated to match the application theme:</p>\n<p>| Property | Before | After |</p>\n<p>|----------|--------|-------|</p>\n<p>| Form input border-radius | `10px` | `12px` (matches `--radius-sm`) |</p>\n<p>| Form input padding | `8px 10px` | `11px 14px` |</p>\n<p>| Field label font-size | `0.73rem` + uppercase | `0.82rem`, no forced uppercase |</p>\n<p>| Field label letter-spacing | `0.07em` | `0.04em` |</p>\n<p>| Focus ring | `outline: 2px solid var(--accent)` | `outline: 2px solid rgba(175,77,49,0.3)` |</p>\n<p>| Panel header background | `rgba(244,239,231,0.6)` hardcoded | `var(--panel)` |</p>\n<p>| Toolbar background | `rgba(244,239,231,0.7)` hardcoded | `rgba(255,255,255,0.55)` |</p>\n<p>| Action bar background | `rgba(244,239,231,0.4)` hardcoded | `rgba(255,255,255,0.25)` |</p>\n<p>| Save button | `background: var(--accent)` | `linear-gradient(135deg, var(--accent), var(--accent-deep))` |</p>\n<p>| Action buttons | `border-radius: 7px` | `border-radius: 999px` pill shape |</p>\n<p>| Active library row tint | `rgba(26,122,154,0.06)` teal | `rgba(175,77,49,0.07)` accent |</p>\n<p>| Type badge (markdown) | `rgba(26,122,154,0.1)` teal | `rgba(175,77,49,0.1)` accent |</p>\n<p>| File notice tint | `rgba(26,122,154,0.06)` teal | `rgba(175,77,49,0.05)` accent |</p>\n<p>---</p>\n<h2>9. `document_service.php` — Parse Error Fix</h2>\n<h3>Problem</h3>\n<p>The function `app_backend_documents_dir()` had its closing `return $path; }` accidentally</p>\n<p>displaced to after `app_document_can_edit_inline()` (lines 147–148), with the rest of the</p>\n<p>file&#039;s functions (`app_libreoffice_convert_to_html`, etc.) nested inside the unclosed</p>\n<p>function body. This caused a PHP parse error on every request to any page that</p>\n<p>`require_once`&#039;s `document_service.php`, making `app_document_can_edit_inline()`</p>\n<p>appear undefined.</p>\n<h3>Fix</h3>\n<p>Two edits to `document_service.php`:</p>\n<p>1. Closed `app_backend_documents_dir()` properly after the `mkdir` block:</p>\n<p>```php</p>\n<p>function app_backend_documents_dir(): string</p>\n<p>{</p>\n<p>$path = __DIR__ . &#039;/documents_storage&#039;;</p>\n<p>if (!is_dir($path)) {</p>\n<p>mkdir($path, 0775, true);</p>\n<p>}</p>\n<p>return $path;   // ← added</p>\n<p>}                   // ← added</p>\n<p>```</p>\n<p>2. Removed the orphaned `return $path; }` that had been displaced to after</p>\n<p>`app_document_can_edit_inline()` at lines 147–148.</p>\n<p>---</p>\n<h2>Access URLs</h2>\n<p>| Module | URL |</p>\n<p>|--------|-----|</p>\n<p>| Backend Home | `/backend/` |</p>\n<p>| Bills Manager | `/backend/bills.php` |</p>\n<p>| Stocks &amp; Crypto | `/backend/stocks_crypto.php` |</p>\n<p>| Cron Manager | `/backend/cron_manager.php` |</p>\n<p>| Error Log | `/backend/error_log.php` |</p>\n<p>| Change Log | `/backend/change_log.php` |</p>\n<p>| Documents | `/backend/documents.php` |</p>\n<p>| Documents API | `/backend/api/documents.php` |</p>\n<p>---</p>\n<h2>Technology Used</h2>\n<p>| Library | Version | Purpose |</p>\n<p>|---------|---------|---------|</p>\n<p>| EasyMDE | latest (unpkg CDN) | Rich markdown editor |</p>\n<p>| marked.js | latest (jsdelivr CDN) | Client-side markdown → HTML rendering |</p>\n<p>| Yahoo Finance API | v8 (unofficial) | Live stock/ETF price fetching |</p>\n<p>| PHPMailer | (existing) | Email sending via `app_send_mail()` |</p>\n<p>---</p>\n<h2>Coding Conventions (consistent with existing codebase)</h2>\n<ul>\n<li>`declare(strict_types=1)` in all PHP files</li>\n<li>`app_db()` for database connections, `mysqli` throughout</li>\n<li>`app_query_all()`, `app_query_one()`, `app_h()`, `app_send_json()` helpers</li>\n<li>`app_log_backend_exception()` for error logging</li>\n<li>POST/Redirect/GET for all form submissions</li>\n<li>`ON DUPLICATE KEY UPDATE` for safe upserts</li>\n<li>`app_backend_render_header()` / `app_backend_render_footer()` for page chrome</li>\n<li>CSS custom properties: `--accent`, `--panel`, `--line`, `--text`, `--muted`, `--shadow`</li>\n<li>All new tables use `CREATE TABLE IF NOT EXISTS` (idempotent, no separate migration needed)</li>\n</ul>","file_name":"SESSION_LOG_2026-04-07.md","stored_name":"20260407-190624-56f0e818.md","mime_type":"application/octet-stream","file_size_bytes":"21112","storage_path":"/mnt/drive1/customerdb/backend/documents_storage/20260407-190624-56f0e818.md","is_deleted":"0","created_at":"2026-04-07 15:06:24","updated_at":"2026-04-07 15:08:09","editor_content":"# Session Log — 2026-04-07\r\n\r\nAll work completed in this session on the CustomerDB backend application.\r\n\r\n---\r\n\r\n## Summary of New Modules and Changes\r\n\r\n| Area | File(s) | Status |\r\n|------|---------|--------|\r\n| Bills Manager | `backend/bills_service.php`, `backend/bills.php` | New |\r\n| Next-Day Email Job | `backend/jobs/send_nextday_customer_emails.php` | New |\r\n| Cron Manager UI | `backend/cron_manager.php` | New |\r\n| Error Log UI | `backend/error_log.php` | New |\r\n| Change Log UI | `backend/change_log.php` | New |\r\n| Stocks & Crypto | `backend/stocks_crypto_service.php`, `backend/stocks_crypto.php` | New |\r\n| Backend Home | `backend/index.php` | Modified |\r\n| Documents Editor | `backend/documents.php` | Full rewrite |\r\n| Documents Service | `backend/document_service.php` | Bug fix |\r\n\r\n---\r\n\r\n## 1. Bills Manager\r\n\r\n### Files\r\n- `backend/bills_service.php` — 356 lines, service layer\r\n- `backend/bills.php` — 972 lines, UI page\r\n\r\n### Purpose\r\nReplaces a spreadsheet (`newbills2026.xlsx`) with a web-based bill payment tracker.\r\nTracks bank account opening balances, monthly bill payments, and computes running bank\r\nbalances in the same layout as the Excel spreadsheet.\r\n\r\n### Database Tables (auto-created on first load)\r\n\r\n**`bills_account`**\r\n| Column | Type | Notes |\r\n|--------|------|-------|\r\n| `bills_account_id` | INT PK AUTO | |\r\n| `name` | VARCHAR(120) | Bank name |\r\n| `sort_order` | INT | Display order |\r\n\r\n**`bills_item`**\r\n| Column | Type | Notes |\r\n|--------|------|-------|\r\n| `bills_item_id` | INT PK AUTO | |\r\n| `label` | VARCHAR(200) | Bill name |\r\n| `default_amount` | DECIMAL(12,2) | Pre-fills the Due column |\r\n| `default_account_id` | INT | Default bank to debit |\r\n| `notes` | TEXT | Instructions / URLs |\r\n| `sort_order` | INT | Row order |\r\n| `is_deleted` | TINYINT | Soft delete |\r\n\r\n**`bills_entry`**\r\nMonthly bill payment rows.\r\n\r\n| Column | Type | Notes |\r\n|--------|------|-------|\r\n| `bills_entry_id` | INT PK AUTO | |\r\n| `bills_item_id` | INT | FK to `bills_item` |\r\n| `year`, `month` | INT | Billing period |\r\n| `amount_due` | DECIMAL(12,2) | Amount billed |\r\n| `amount_paid` | DECIMAL(12,2) | Amount actually paid |\r\n| `account_id` | INT | Bank used to pay |\r\n\r\n**`bills_balance`**\r\nOpening bank balances per month.\r\n\r\n| Column | Type | Notes |\r\n|--------|------|-------|\r\n| `bills_balance_id` | INT PK AUTO | |\r\n| `account_id` | INT | FK to `bills_account` |\r\n| `year`, `month` | INT | Period |\r\n| `opening_balance` | DECIMAL(14,2) | Balance at start of month |\r\n\r\n### Seed Data\r\nOn first load (empty table), the following are auto-seeded from the original Excel file:\r\n\r\n**Accounts:** USAA, MIDFLORIDA, Centenial, CapitalOne\r\n\r\n**Bills (11 items):** MidFlorida Mortgage, MidFlorida Credit Card, USAA Credit Card,\r\nCentenial Bank, Electricity, Waste Management, Comcast/Xfinity, Insurance, Water,\r\nCapitalOne #1, CapitalOne #2\r\n\r\n### Service Functions\r\n\r\n```php\r\napp_install_bills_schema(mysqli $conn): void\r\napp_bills_seed_defaults(mysqli $conn): void\r\napp_bills_accounts(mysqli $conn): array\r\napp_bills_all_accounts(mysqli $conn): array\r\napp_bills_items(mysqli $conn): array\r\napp_bills_balances(mysqli $conn, int $year, int $month): array       // map: account_id → float\r\napp_bills_entries(mysqli $conn, int $year, int $month): array        // map: item_id → [due, paid, account_id]\r\napp_bills_active_months(mysqli $conn, int $year): array\r\napp_bills_save_balance(mysqli $conn, int $year, int $month, int $accountId, float $balance): void\r\napp_bills_save_entry(mysqli $conn, int $year, int $month, int $itemId, ?float $due, ?float $paid, ?int $accountId): void\r\napp_bills_save_account(mysqli $conn, array $data): int\r\napp_bills_delete_account(mysqli $conn, int $id): void\r\napp_bills_save_item(mysqli $conn, array $data): int\r\napp_bills_delete_item(mysqli $conn, int $id): void\r\napp_bills_reorder_item(mysqli $conn, int $id, string $dir): void\r\napp_bills_copy_month(mysqli $conn, int $fromYear, int $fromMonth, int $toYear, int $toMonth): int\r\n```\r\n\r\n### UI Features (`bills.php`)\r\n- Month/year navigation with ◀ / ▶ buttons and active-month pills\r\n- **Bank Balances section** — editable opening balance per account, inline Save button\r\n- **Bills table** — spreadsheet-style rows with:\r\n  - Amount Due (editable, pre-filled from default)\r\n  - Amount Paid (editable, AJAX auto-save after 600 ms debounce)\r\n  - Account selector (which bank pays this bill)\r\n  - Balance After column (computed client-side in real time)\r\n  - Up/Down sort buttons\r\n  - Edit and Delete per row\r\n- **Sticky running totals sidebar** — shows each bank's current balance as bills are entered\r\n- **Copy from previous month** — modal to copy all entries from a prior month to the current one\r\n- **Add Bill / Edit Bill modals**\r\n- **Add Account / Edit Account modals**\r\n- All saves use POST/Redirect/GET; bill entry changes auto-save via `fetch()` AJAX\r\n\r\n### Running Total Calculation\r\nJavaScript function `recalcTotals()` walks bills in DOM order, maintains a\r\n`bal[accountId]` map starting from opening balances, subtracts `amount_paid` from the\r\nselected account for each bill, and updates the \"Balance After\" column and sidebar panel\r\nin real time — matching the Excel spreadsheet formula logic.\r\n\r\n---\r\n\r\n## 2. Next-Day Customer Email Job\r\n\r\n### File\r\n- `backend/jobs/send_nextday_customer_emails.php` — 158 lines\r\n\r\n### Purpose\r\nPHP CLI translation of the VBA macro `RunNextDayCustomerEmails()`.\r\nQueries `setmore_appointments` for appointments scheduled for tomorrow\r\n(status ≠ Cancelled) and sends a reminder email to each customer.\r\n\r\n### Usage\r\n```bash\r\n# Normal run (sends real emails)\r\nphp backend/jobs/send_nextday_customer_emails.php\r\n\r\n# Dry run — prints what would be sent, no emails sent\r\nphp backend/jobs/send_nextday_customer_emails.php --dry-run\r\n\r\n# Override date (for testing a specific date)\r\nphp backend/jobs/send_nextday_customer_emails.php --date=2026-04-09\r\n```\r\n\r\n### Recommended Cron Schedule\r\n```\r\n0 20 * * *  php /path/to/backend/jobs/send_nextday_customer_emails.php\r\n```\r\nRuns at 8:00 PM every evening, sends reminders for the following day's appointments.\r\n\r\n### Email Content\r\n- Subject: `Reminder: Your appointment at Ella's Alterations tomorrow`\r\n- Body matches the original VBA email with full address, phone, 5-line award\r\n  signature block, and a plain-text fallback\r\n- Uses `app_send_mail()` / PHPMailer\r\n\r\n---\r\n\r\n## 3. Cron Manager UI\r\n\r\n### File\r\n- `backend/cron_manager.php` — 482 lines\r\n\r\n### Purpose\r\nWeb UI for managing scheduled background jobs. Stores job definitions in the database,\r\nallows toggling on/off, running immediately, editing schedules, and exporting to\r\n`/etc/cron.d/` format.\r\n\r\n### Database Table (auto-created)\r\n\r\n**`cron_job`**\r\n| Column | Type | Notes |\r\n|--------|------|-------|\r\n| `cron_job_id` | INT PK AUTO | |\r\n| `label` | VARCHAR(120) | Human name |\r\n| `description` | TEXT | What the job does |\r\n| `schedule` | VARCHAR(100) | Cron expression e.g. `0 20 * * *` |\r\n| `command` | VARCHAR(500) | Shell command to run |\r\n| `enabled` | TINYINT | 0 = disabled, 1 = enabled |\r\n| `last_run_at` | DATETIME | Updated on \"Run Now\" |\r\n| `last_output` | TEXT | Captured stdout/stderr |\r\n| `sort_order` | INT | Display order |\r\n\r\n### Seeded Jobs (on first install)\r\n1. **Next-Day Customer Emails** — `0 20 * * *` — `php backend/jobs/send_nextday_customer_emails.php`\r\n2. **Morning Jobs** — `0 7 * * *` — `php backend/jobs/morning_jobs.php`\r\n3. **Nightly Reports** — `0 23 * * *` — `php backend/jobs/nightly_reports.php`\r\n4. **DB Backup** — `0 2 * * *` — `bash backend/bin/db_backup.sh`\r\n5. **Daily Cleanup** — `30 3 * * *` — `php backend/jobs/daily_cleanup.php`\r\n6. **Add Name Records Auto Delete** — `0 4 * * *` — `php backend/jobs/cleanup_name_records.php`\r\n\r\nNew jobs are added idempotently (checked by label before inserting) so existing\r\ninstallations are not re-seeded.\r\n\r\n### UI Features\r\n- Table listing all jobs with schedule, enabled toggle, last run time\r\n- **Toggle** — flip enabled/disabled immediately (POST/redirect)\r\n- **▶ Run Now** — executes the command via `exec()`, captures output, stores to DB, shows in modal\r\n- **Add / Edit** — form with label, description, command, schedule, enabled flag\r\n- **Schedule presets** — quick-select buttons (hourly, daily at common times, weekly, monthly)\r\n- `cron_describe(string $expr): string` — converts `0 20 * * *` → \"Daily at 20:00\"\r\n- **Export Crontab** — writes `/tmp/customerdb_crontab_export.txt` in `/etc/cron.d/` format\r\n\r\n---\r\n\r\n## 4. Error Log UI\r\n\r\n### File\r\n- `backend/error_log.php` — 262 lines\r\n\r\n### Purpose\r\nDedicated UI for browsing `backend_error_log` records with filtering, pagination,\r\nand per-entry management.\r\n\r\n### Features\r\n- **Stats bar** — Total errors, Today's errors, Latest error timestamp\r\n- **Filter bar** — Search (message/context), Source dropdown, Severity dropdown\r\n- **Pagination** — 50 per page with smart ellipsis navigator\r\n- **Per-row delete** — removes individual log entries\r\n- **Clear All button** — truncates the entire log (with confirmation)\r\n- JSON context blocks displayed in `<details>` collapsibles with dark monospace styling\r\n- URL: `/backend/error_log.php`\r\n\r\n---\r\n\r\n## 5. Change Log UI\r\n\r\n### File\r\n- `backend/change_log.php` — 248 lines\r\n\r\n### Purpose\r\nDedicated UI for browsing `backend_change_log` records — audit trail of all\r\ndata changes across the application.\r\n\r\n### Features\r\n- **Filter bar** — Search, Area, Action type, Entity type dropdowns\r\n- **Pagination** — smart ellipsis navigator\r\n- **Per-row detail panels:**\r\n  - Before / After / Context — collapsed JSON blocks\r\n  - Changed Data — open by default, shows the changed fields summary\r\n- Action badges color-coded by type (create, update, delete)\r\n- URL: `/backend/change_log.php`\r\n\r\n---\r\n\r\n## 6. Stocks & Crypto Module\r\n\r\n### Files\r\n- `backend/stocks_crypto_service.php` — 269 lines, service layer\r\n- `backend/stocks_crypto.php` — 667 lines, UI page\r\n\r\n### Purpose\r\nTrack a personal investment portfolio — individual stocks, ETFs, precious metals,\r\nand crypto. Supports live price fetching, manual daily price entry, monthly crypto\r\nbuy tracking, and portfolio gain/loss analysis.\r\n\r\n### Database Tables (auto-created)\r\n\r\n**`stocks_asset`**\r\n| Column | Type | Notes |\r\n|--------|------|-------|\r\n| `stocks_asset_id` | INT PK AUTO | |\r\n| `ticker` | VARCHAR(20) | e.g. `AAPL`, `BTC`, `GLD` |\r\n| `name` | VARCHAR(120) | Display name |\r\n| `asset_type` | ENUM | `stock`, `etf`, `crypto`, `metal`, `other` |\r\n| `shares` | DECIMAL(18,8) | Quantity owned |\r\n| `cost_basis_total` | DECIMAL(14,6) | Total purchase cost |\r\n| `is_active` | TINYINT | Soft hide |\r\n\r\n**`stocks_price`**\r\nDaily price records.\r\n\r\n| Column | Type | Notes |\r\n|--------|------|-------|\r\n| `stocks_price_id` | INT PK AUTO | |\r\n| `ticker` | VARCHAR(20) | |\r\n| `price_date` | DATE | |\r\n| `price` | DECIMAL(14,6) | |\r\n| `source` | VARCHAR(40) | `manual` or `yahoo` |\r\n\r\nUnique constraint on `(ticker, price_date)` — upserts with `ON DUPLICATE KEY UPDATE`.\r\n\r\n**`crypto_monthly_buy`**\r\n| Column | Type | Notes |\r\n|--------|------|-------|\r\n| `crypto_monthly_buy_id` | INT PK AUTO | |\r\n| `ticker` | VARCHAR(20) | |\r\n| `year`, `month` | INT | |\r\n| `quantity` | DECIMAL(18,8) | Crypto units purchased |\r\n| `amount_usd` | DECIMAL(14,2) | USD spent |\r\n| `notes` | VARCHAR(255) | |\r\n\r\n### Service Functions\r\n\r\n```php\r\napp_install_stocks_schema(mysqli $conn): void\r\napp_stocks_assets(mysqli $conn, string $type = ''): array\r\napp_stocks_save_asset(mysqli $conn, array $data): int\r\napp_stocks_delete_asset(mysqli $conn, int $id): void\r\napp_stocks_save_price(mysqli $conn, string $ticker, string $date, float $price, string $source = 'manual'): void\r\napp_stocks_save_prices_bulk(mysqli $conn, string $date, array $prices): int\r\napp_stocks_latest_prices(mysqli $conn): array                          // map: ticker → [price, date]\r\napp_stocks_price_history(mysqli $conn, string $ticker, int $limit = 90): array\r\napp_stocks_fetch_live_price(string $ticker): ?float                    // Yahoo Finance API, 8s timeout\r\napp_stocks_fetch_all_live(mysqli $conn): array                         // fetches all active tickers\r\napp_crypto_monthly_buys(mysqli $conn, int $year, int $month = 0): array\r\napp_crypto_save_monthly_buy(mysqli $conn, array $data): void\r\napp_crypto_delete_monthly_buy(mysqli $conn, int $id): void\r\napp_stocks_dashboard(mysqli $conn): array                              // cost_basis, current_value, gain_loss, gain_pct per asset\r\napp_stocks_stats(mysqli $conn): array                                  // total portfolio stats\r\n```\r\n\r\n### Live Price Source\r\nYahoo Finance unofficial API:\r\n```\r\nhttps://query1.finance.yahoo.com/v8/finance/chart/{TICKER}?interval=1d&range=1d\r\n```\r\n8-second timeout, reads `.chart.result[0].meta.regularMarketPrice`.\r\n\r\n### UI Tabs (`stocks_crypto.php`)\r\n\r\n1. **Portfolio** — dashboard table with ticker, shares, cost basis, current value,\r\n   gain/loss, gain %, latest price date. Stats bar shows total value, total cost,\r\n   total gain/loss. \"Fetch Live Prices\" button calls Yahoo Finance for all tickers.\r\n\r\n2. **Enter Prices** — manual daily price entry grid for all active assets,\r\n   date picker (defaults to today), bulk save.\r\n\r\n3. **Crypto Buys** — monthly crypto purchase log. Month/year selector, table of\r\n   ticker/quantity/USD/notes rows, add and delete per entry.\r\n\r\n4. **Add Asset** — form to add a new stock, ETF, crypto, metal, or other asset.\r\n\r\n### Auto-Refresh\r\nA checkbox above the stats bar auto-refreshes the Portfolio tab every 10 seconds.\r\nState persists in `localStorage` key `sc_auto_refresh`. On reload, strips `?ok=`\r\nand `?warn=` query params to prevent the notice from repeating every 10 seconds.\r\n\r\n---\r\n\r\n## 7. Backend Home — New Cards (`index.php`)\r\n\r\nSix new cards added to the backend home dashboard:\r\n\r\n| Card | Link | Secondary Action |\r\n|------|------|-----------------|\r\n| Customer Totals | `/backend/customer_totals.php` | — |\r\n| Ambient Weather | `/backend/ambient_weather.php` | — |\r\n| Bills Manager | `/backend/bills.php` | — |\r\n| Stocks & Crypto | `/backend/stocks_crypto.php` | — |\r\n| Cron Manager | `/backend/cron_manager.php` | — |\r\n| Error Log | `/backend/error_log.php` | Change Log |\r\n\r\n---\r\n\r\n## 8. Documents Editor — Full Rewrite (`documents.php`)\r\n\r\n### Problem\r\nGitHub Copilot introduced several bugs into the previous version:\r\n- `docLoad()` had mismatched closing braces, breaking the entire function\r\n- The document library was constrained to ~40% page width inside a grid column\r\n- The editor was a plain `<textarea>` with no formatting support\r\n\r\n### Solution\r\nComplete rewrite of all HTML, CSS, and JavaScript. PHP POST handlers were kept\r\nidentical — only the front-end was replaced.\r\n\r\n### Editor: EasyMDE\r\nReplaced the plain textarea with **EasyMDE** (Easy Markdown Editor) via CDN:\r\n```html\r\n<link rel=\"stylesheet\" href=\"https://unpkg.com/easymde/dist/easymde.min.css\">\r\n<script src=\"https://unpkg.com/easymde/dist/easymde.min.js\"></script>\r\n```\r\n\r\nEasyMDE wraps the `<textarea id=\"content_markdown\">` and syncs content on form submit.\r\n`ta.value = easyMDE.value()` is called explicitly in `submitDoc()` before `form.submit()`.\r\n\r\n### Toolbar Buttons\r\nSave (custom gradient btn) | Bold · Italic · Strikethrough | H1 · H2 · H3 |\r\nUnordered List · Ordered List · Checklist (custom) | Quote · Code · Table |\r\nHR · Link · Image URL (custom) | Undo · Redo | Fullscreen | Guide\r\n\r\n- **Save** button in toolbar calls `submitDoc()`, same as the Save Document button below\r\n- **Ctrl+S / Cmd+S** keyboard shortcut via CodeMirror `extraKeys`\r\n- **Checklist** inserts `- [ ] ` prefix on selected text\r\n- **Image URL** prompts for URL and alt text, inserts `![alt](url)` syntax\r\n\r\n### Preview Toggle\r\nEasyMDE's built-in `preview` and `side-by-side` buttons were **removed** to prevent\r\nboth raw markdown and rendered HTML showing simultaneously.\r\n\r\nA custom **Preview** button in the panel header replaces them:\r\n- Uses `marked.js` (CDN) to render the markdown client-side\r\n- **Preview on**: hides `EasyMDEContainer` and highlight strip; shows `#doc-preview-pane`\r\n- **Edit on**: hides preview pane; shows EasyMDE and highlight strip; calls `codemirror.refresh()`\r\n- Loading a document via `docLoad()` automatically returns to edit mode\r\n- Button state: pill shape, switches between \"👁 Preview\" and \"✏ Edit\" labels\r\n\r\n```html\r\n<script src=\"https://cdn.jsdelivr.net/npm/marked/marked.min.js\"></script>\r\n```\r\n\r\n### `docLoad()` — Clean Rewrite\r\nOld function had mismatched braces and redundant `editor.value` assignments.\r\nNew function:\r\n1. Returns to edit mode if preview is on\r\n2. Sets heading to \"Loading…\"\r\n3. Highlights active `<tr>` and `<a>` in library immediately\r\n4. `fetch('/backend/api/documents.php?backend_document_id=' + id)`\r\n5. Populates all meta fields (type, title, slug, summary)\r\n6. Sets `easyMDE.value(content)` from `editor_content` or `content_markdown`\r\n7. Shows/hides file notice\r\n8. Calls `renderVersionList(data.versions)`\r\n9. Updates URL via `history.replaceState()`\r\n10. Flashes panel border with accent color for 800 ms\r\n\r\n### Library Panel — Full Width\r\nOld layout: library was inside a 2-column grid alongside the editor.\r\nNew layout:\r\n```\r\n[Editor Panel          — full width]\r\n[Library Panel         — full width, max-height: 460px scrollable]\r\n[Upload | Versions     — 2-column row]\r\n```\r\n\r\nLive client-side search filters table rows by `textContent` without a page reload.\r\n\r\n### Word Count & Highlights\r\n- Word count and char count update on every keystroke via `codemirror.on('change', ...)`\r\n- 6 highlight color swatches + a clear option wrap selected text in\r\n  `<span class=\"hl\" style=\"background:COLOR\">...</span>`\r\n- Clear highlight strips existing `<span class=\"hl\">` wrappers from selected text\r\n\r\n### CSS Theme Conformity (visit.html standard)\r\nAll custom CSS was updated to match the application theme:\r\n\r\n| Property | Before | After |\r\n|----------|--------|-------|\r\n| Form input border-radius | `10px` | `12px` (matches `--radius-sm`) |\r\n| Form input padding | `8px 10px` | `11px 14px` |\r\n| Field label font-size | `0.73rem` + uppercase | `0.82rem`, no forced uppercase |\r\n| Field label letter-spacing | `0.07em` | `0.04em` |\r\n| Focus ring | `outline: 2px solid var(--accent)` | `outline: 2px solid rgba(175,77,49,0.3)` |\r\n| Panel header background | `rgba(244,239,231,0.6)` hardcoded | `var(--panel)` |\r\n| Toolbar background | `rgba(244,239,231,0.7)` hardcoded | `rgba(255,255,255,0.55)` |\r\n| Action bar background | `rgba(244,239,231,0.4)` hardcoded | `rgba(255,255,255,0.25)` |\r\n| Save button | `background: var(--accent)` | `linear-gradient(135deg, var(--accent), var(--accent-deep))` |\r\n| Action buttons | `border-radius: 7px` | `border-radius: 999px` pill shape |\r\n| Active library row tint | `rgba(26,122,154,0.06)` teal | `rgba(175,77,49,0.07)` accent |\r\n| Type badge (markdown) | `rgba(26,122,154,0.1)` teal | `rgba(175,77,49,0.1)` accent |\r\n| File notice tint | `rgba(26,122,154,0.06)` teal | `rgba(175,77,49,0.05)` accent |\r\n\r\n---\r\n\r\n## 9. `document_service.php` — Parse Error Fix\r\n\r\n### Problem\r\nThe function `app_backend_documents_dir()` had its closing `return $path; }` accidentally\r\ndisplaced to after `app_document_can_edit_inline()` (lines 147–148), with the rest of the\r\nfile's functions (`app_libreoffice_convert_to_html`, etc.) nested inside the unclosed\r\nfunction body. This caused a PHP parse error on every request to any page that\r\n`require_once`'s `document_service.php`, making `app_document_can_edit_inline()`\r\nappear undefined.\r\n\r\n### Fix\r\nTwo edits to `document_service.php`:\r\n\r\n1. Closed `app_backend_documents_dir()` properly after the `mkdir` block:\r\n```php\r\nfunction app_backend_documents_dir(): string\r\n{\r\n    $path = __DIR__ . '/documents_storage';\r\n    if (!is_dir($path)) {\r\n        mkdir($path, 0775, true);\r\n    }\r\n    return $path;   // ← added\r\n}                   // ← added\r\n```\r\n\r\n2. Removed the orphaned `return $path; }` that had been displaced to after\r\n`app_document_can_edit_inline()` at lines 147–148.\r\n\r\n---\r\n\r\n## Access URLs\r\n\r\n| Module | URL |\r\n|--------|-----|\r\n| Backend Home | `/backend/` |\r\n| Bills Manager | `/backend/bills.php` |\r\n| Stocks & Crypto | `/backend/stocks_crypto.php` |\r\n| Cron Manager | `/backend/cron_manager.php` |\r\n| Error Log | `/backend/error_log.php` |\r\n| Change Log | `/backend/change_log.php` |\r\n| Documents | `/backend/documents.php` |\r\n| Documents API | `/backend/api/documents.php` |\r\n\r\n---\r\n\r\n## Technology Used\r\n\r\n| Library | Version | Purpose |\r\n|---------|---------|---------|\r\n| EasyMDE | latest (unpkg CDN) | Rich markdown editor |\r\n| marked.js | latest (jsdelivr CDN) | Client-side markdown → HTML rendering |\r\n| Yahoo Finance API | v8 (unofficial) | Live stock/ETF price fetching |\r\n| PHPMailer | (existing) | Email sending via `app_send_mail()` |\r\n\r\n---\r\n\r\n## Coding Conventions (consistent with existing codebase)\r\n\r\n- `declare(strict_types=1)` in all PHP files\r\n- `app_db()` for database connections, `mysqli` throughout\r\n- `app_query_all()`, `app_query_one()`, `app_h()`, `app_send_json()` helpers\r\n- `app_log_backend_exception()` for error logging\r\n- POST/Redirect/GET for all form submissions\r\n- `ON DUPLICATE KEY UPDATE` for safe upserts\r\n- `app_backend_render_header()` / `app_backend_render_footer()` for page chrome\r\n- CSS custom properties: `--accent`, `--panel`, `--line`, `--text`, `--muted`, `--shadow`\r\n- All new tables use `CREATE TABLE IF NOT EXISTS` (idempotent, no separate migration needed)","is_text_editable":1,"can_edit_inline":1}
documents · upload
2026-04-07 15:06:24 · anonymous · /backend/documents.php?backend_document_id=8
change
backend_document #10
Context
{"file_name":"SESSION_LOG_2026-04-07.md","mime_type":"application/octet-stream"}
Before
[]
After
{"backend_document_id":"10","document_type":"upload","title":"Claude 04-07-2026","slug":"claude-04-07-2026","summary_text":"Claude 04-07-2026 fixing backend Document areas","content_markdown":null,"content_html":null,"file_name":"SESSION_LOG_2026-04-07.md","stored_name":"20260407-190624-56f0e818.md","mime_type":"application/octet-stream","file_size_bytes":"21112","storage_path":"/mnt/drive1/customerdb/backend/documents_storage/20260407-190624-56f0e818.md","is_deleted":"0","created_at":"2026-04-07 15:06:24","updated_at":"2026-04-07 15:06:24","editor_content":"# Session Log — 2026-04-07\n\nAll work completed in this session on the CustomerDB backend application.\n\n---\n\n## Summary of New Modules and Changes\n\n| Area | File(s) | Status |\n|------|---------|--------|\n| Bills Manager | `backend/bills_service.php`, `backend/bills.php` | New |\n| Next-Day Email Job | `backend/jobs/send_nextday_customer_emails.php` | New |\n| Cron Manager UI | `backend/cron_manager.php` | New |\n| Error Log UI | `backend/error_log.php` | New |\n| Change Log UI | `backend/change_log.php` | New |\n| Stocks & Crypto | `backend/stocks_crypto_service.php`, `backend/stocks_crypto.php` | New |\n| Backend Home | `backend/index.php` | Modified |\n| Documents Editor | `backend/documents.php` | Full rewrite |\n| Documents Service | `backend/document_service.php` | Bug fix |\n\n---\n\n## 1. Bills Manager\n\n### Files\n- `backend/bills_service.php` — 356 lines, service layer\n- `backend/bills.php` — 972 lines, UI page\n\n### Purpose\nReplaces a spreadsheet (`newbills2026.xlsx`) with a web-based bill payment tracker.\nTracks bank account opening balances, monthly bill payments, and computes running bank\nbalances in the same layout as the Excel spreadsheet.\n\n### Database Tables (auto-created on first load)\n\n**`bills_account`**\n| Column | Type | Notes |\n|--------|------|-------|\n| `bills_account_id` | INT PK AUTO | |\n| `name` | VARCHAR(120) | Bank name |\n| `sort_order` | INT | Display order |\n\n**`bills_item`**\n| Column | Type | Notes |\n|--------|------|-------|\n| `bills_item_id` | INT PK AUTO | |\n| `label` | VARCHAR(200) | Bill name |\n| `default_amount` | DECIMAL(12,2) | Pre-fills the Due column |\n| `default_account_id` | INT | Default bank to debit |\n| `notes` | TEXT | Instructions / URLs |\n| `sort_order` | INT | Row order |\n| `is_deleted` | TINYINT | Soft delete |\n\n**`bills_entry`**\nMonthly bill payment rows.\n\n| Column | Type | Notes |\n|--------|------|-------|\n| `bills_entry_id` | INT PK AUTO | |\n| `bills_item_id` | INT | FK to `bills_item` |\n| `year`, `month` | INT | Billing period |\n| `amount_due` | DECIMAL(12,2) | Amount billed |\n| `amount_paid` | DECIMAL(12,2) | Amount actually paid |\n| `account_id` | INT | Bank used to pay |\n\n**`bills_balance`**\nOpening bank balances per month.\n\n| Column | Type | Notes |\n|--------|------|-------|\n| `bills_balance_id` | INT PK AUTO | |\n| `account_id` | INT | FK to `bills_account` |\n| `year`, `month` | INT | Period |\n| `opening_balance` | DECIMAL(14,2) | Balance at start of month |\n\n### Seed Data\nOn first load (empty table), the following are auto-seeded from the original Excel file:\n\n**Accounts:** USAA, MIDFLORIDA, Centenial, CapitalOne\n\n**Bills (11 items):** MidFlorida Mortgage, MidFlorida Credit Card, USAA Credit Card,\nCentenial Bank, Electricity, Waste Management, Comcast/Xfinity, Insurance, Water,\nCapitalOne #1, CapitalOne #2\n\n### Service Functions\n\n```php\napp_install_bills_schema(mysqli $conn): void\napp_bills_seed_defaults(mysqli $conn): void\napp_bills_accounts(mysqli $conn): array\napp_bills_all_accounts(mysqli $conn): array\napp_bills_items(mysqli $conn): array\napp_bills_balances(mysqli $conn, int $year, int $month): array       // map: account_id → float\napp_bills_entries(mysqli $conn, int $year, int $month): array        // map: item_id → [due, paid, account_id]\napp_bills_active_months(mysqli $conn, int $year): array\napp_bills_save_balance(mysqli $conn, int $year, int $month, int $accountId, float $balance): void\napp_bills_save_entry(mysqli $conn, int $year, int $month, int $itemId, ?float $due, ?float $paid, ?int $accountId): void\napp_bills_save_account(mysqli $conn, array $data): int\napp_bills_delete_account(mysqli $conn, int $id): void\napp_bills_save_item(mysqli $conn, array $data): int\napp_bills_delete_item(mysqli $conn, int $id): void\napp_bills_reorder_item(mysqli $conn, int $id, string $dir): void\napp_bills_copy_month(mysqli $conn, int $fromYear, int $fromMonth, int $toYear, int $toMonth): int\n```\n\n### UI Features (`bills.php`)\n- Month/year navigation with ◀ / ▶ buttons and active-month pills\n- **Bank Balances section** — editable opening balance per account, inline Save button\n- **Bills table** — spreadsheet-style rows with:\n  - Amount Due (editable, pre-filled from default)\n  - Amount Paid (editable, AJAX auto-save after 600 ms debounce)\n  - Account selector (which bank pays this bill)\n  - Balance After column (computed client-side in real time)\n  - Up/Down sort buttons\n  - Edit and Delete per row\n- **Sticky running totals sidebar** — shows each bank's current balance as bills are entered\n- **Copy from previous month** — modal to copy all entries from a prior month to the current one\n- **Add Bill / Edit Bill modals**\n- **Add Account / Edit Account modals**\n- All saves use POST/Redirect/GET; bill entry changes auto-save via `fetch()` AJAX\n\n### Running Total Calculation\nJavaScript function `recalcTotals()` walks bills in DOM order, maintains a\n`bal[accountId]` map starting from opening balances, subtracts `amount_paid` from the\nselected account for each bill, and updates the \"Balance After\" column and sidebar panel\nin real time — matching the Excel spreadsheet formula logic.\n\n---\n\n## 2. Next-Day Customer Email Job\n\n### File\n- `backend/jobs/send_nextday_customer_emails.php` — 158 lines\n\n### Purpose\nPHP CLI translation of the VBA macro `RunNextDayCustomerEmails()`.\nQueries `setmore_appointments` for appointments scheduled for tomorrow\n(status ≠ Cancelled) and sends a reminder email to each customer.\n\n### Usage\n```bash\n# Normal run (sends real emails)\nphp backend/jobs/send_nextday_customer_emails.php\n\n# Dry run — prints what would be sent, no emails sent\nphp backend/jobs/send_nextday_customer_emails.php --dry-run\n\n# Override date (for testing a specific date)\nphp backend/jobs/send_nextday_customer_emails.php --date=2026-04-09\n```\n\n### Recommended Cron Schedule\n```\n0 20 * * *  php /path/to/backend/jobs/send_nextday_customer_emails.php\n```\nRuns at 8:00 PM every evening, sends reminders for the following day's appointments.\n\n### Email Content\n- Subject: `Reminder: Your appointment at Ella's Alterations tomorrow`\n- Body matches the original VBA email with full address, phone, 5-line award\n  signature block, and a plain-text fallback\n- Uses `app_send_mail()` / PHPMailer\n\n---\n\n## 3. Cron Manager UI\n\n### File\n- `backend/cron_manager.php` — 482 lines\n\n### Purpose\nWeb UI for managing scheduled background jobs. Stores job definitions in the database,\nallows toggling on/off, running immediately, editing schedules, and exporting to\n`/etc/cron.d/` format.\n\n### Database Table (auto-created)\n\n**`cron_job`**\n| Column | Type | Notes |\n|--------|------|-------|\n| `cron_job_id` | INT PK AUTO | |\n| `label` | VARCHAR(120) | Human name |\n| `description` | TEXT | What the job does |\n| `schedule` | VARCHAR(100) | Cron expression e.g. `0 20 * * *` |\n| `command` | VARCHAR(500) | Shell command to run |\n| `enabled` | TINYINT | 0 = disabled, 1 = enabled |\n| `last_run_at` | DATETIME | Updated on \"Run Now\" |\n| `last_output` | TEXT | Captured stdout/stderr |\n| `sort_order` | INT | Display order |\n\n### Seeded Jobs (on first install)\n1. **Next-Day Customer Emails** — `0 20 * * *` — `php backend/jobs/send_nextday_customer_emails.php`\n2. **Morning Jobs** — `0 7 * * *` — `php backend/jobs/morning_jobs.php`\n3. **Nightly Reports** — `0 23 * * *` — `php backend/jobs/nightly_reports.php`\n4. **DB Backup** — `0 2 * * *` — `bash backend/bin/db_backup.sh`\n5. **Daily Cleanup** — `30 3 * * *` — `php backend/jobs/daily_cleanup.php`\n6. **Add Name Records Auto Delete** — `0 4 * * *` — `php backend/jobs/cleanup_name_records.php`\n\nNew jobs are added idempotently (checked by label before inserting) so existing\ninstallations are not re-seeded.\n\n### UI Features\n- Table listing all jobs with schedule, enabled toggle, last run time\n- **Toggle** — flip enabled/disabled immediately (POST/redirect)\n- **▶ Run Now** — executes the command via `exec()`, captures output, stores to DB, shows in modal\n- **Add / Edit** — form with label, description, command, schedule, enabled flag\n- **Schedule presets** — quick-select buttons (hourly, daily at common times, weekly, monthly)\n- `cron_describe(string $expr): string` — converts `0 20 * * *` → \"Daily at 20:00\"\n- **Export Crontab** — writes `/tmp/customerdb_crontab_export.txt` in `/etc/cron.d/` format\n\n---\n\n## 4. Error Log UI\n\n### File\n- `backend/error_log.php` — 262 lines\n\n### Purpose\nDedicated UI for browsing `backend_error_log` records with filtering, pagination,\nand per-entry management.\n\n### Features\n- **Stats bar** — Total errors, Today's errors, Latest error timestamp\n- **Filter bar** — Search (message/context), Source dropdown, Severity dropdown\n- **Pagination** — 50 per page with smart ellipsis navigator\n- **Per-row delete** — removes individual log entries\n- **Clear All button** — truncates the entire log (with confirmation)\n- JSON context blocks displayed in `<details>` collapsibles with dark monospace styling\n- URL: `/backend/error_log.php`\n\n---\n\n## 5. Change Log UI\n\n### File\n- `backend/change_log.php` — 248 lines\n\n### Purpose\nDedicated UI for browsing `backend_change_log` records — audit trail of all\ndata changes across the application.\n\n### Features\n- **Filter bar** — Search, Area, Action type, Entity type dropdowns\n- **Pagination** — smart ellipsis navigator\n- **Per-row detail panels:**\n  - Before / After / Context — collapsed JSON blocks\n  - Changed Data — open by default, shows the changed fields summary\n- Action badges color-coded by type (create, update, delete)\n- URL: `/backend/change_log.php`\n\n---\n\n## 6. Stocks & Crypto Module\n\n### Files\n- `backend/stocks_crypto_service.php` — 269 lines, service layer\n- `backend/stocks_crypto.php` — 667 lines, UI page\n\n### Purpose\nTrack a personal investment portfolio — individual stocks, ETFs, precious metals,\nand crypto. Supports live price fetching, manual daily price entry, monthly crypto\nbuy tracking, and portfolio gain/loss analysis.\n\n### Database Tables (auto-created)\n\n**`stocks_asset`**\n| Column | Type | Notes |\n|--------|------|-------|\n| `stocks_asset_id` | INT PK AUTO | |\n| `ticker` | VARCHAR(20) | e.g. `AAPL`, `BTC`, `GLD` |\n| `name` | VARCHAR(120) | Display name |\n| `asset_type` | ENUM | `stock`, `etf`, `crypto`, `metal`, `other` |\n| `shares` | DECIMAL(18,8) | Quantity owned |\n| `cost_basis_total` | DECIMAL(14,6) | Total purchase cost |\n| `is_active` | TINYINT | Soft hide |\n\n**`stocks_price`**\nDaily price records.\n\n| Column | Type | Notes |\n|--------|------|-------|\n| `stocks_price_id` | INT PK AUTO | |\n| `ticker` | VARCHAR(20) | |\n| `price_date` | DATE | |\n| `price` | DECIMAL(14,6) | |\n| `source` | VARCHAR(40) | `manual` or `yahoo` |\n\nUnique constraint on `(ticker, price_date)` — upserts with `ON DUPLICATE KEY UPDATE`.\n\n**`crypto_monthly_buy`**\n| Column | Type | Notes |\n|--------|------|-------|\n| `crypto_monthly_buy_id` | INT PK AUTO | |\n| `ticker` | VARCHAR(20) | |\n| `year`, `month` | INT | |\n| `quantity` | DECIMAL(18,8) | Crypto units purchased |\n| `amount_usd` | DECIMAL(14,2) | USD spent |\n| `notes` | VARCHAR(255) | |\n\n### Service Functions\n\n```php\napp_install_stocks_schema(mysqli $conn): void\napp_stocks_assets(mysqli $conn, string $type = ''): array\napp_stocks_save_asset(mysqli $conn, array $data): int\napp_stocks_delete_asset(mysqli $conn, int $id): void\napp_stocks_save_price(mysqli $conn, string $ticker, string $date, float $price, string $source = 'manual'): void\napp_stocks_save_prices_bulk(mysqli $conn, string $date, array $prices): int\napp_stocks_latest_prices(mysqli $conn): array                          // map: ticker → [price, date]\napp_stocks_price_history(mysqli $conn, string $ticker, int $limit = 90): array\napp_stocks_fetch_live_price(string $ticker): ?float                    // Yahoo Finance API, 8s timeout\napp_stocks_fetch_all_live(mysqli $conn): array                         // fetches all active tickers\napp_crypto_monthly_buys(mysqli $conn, int $year, int $month = 0): array\napp_crypto_save_monthly_buy(mysqli $conn, array $data): void\napp_crypto_delete_monthly_buy(mysqli $conn, int $id): void\napp_stocks_dashboard(mysqli $conn): array                              // cost_basis, current_value, gain_loss, gain_pct per asset\napp_stocks_stats(mysqli $conn): array                                  // total portfolio stats\n```\n\n### Live Price Source\nYahoo Finance unofficial API:\n```\nhttps://query1.finance.yahoo.com/v8/finance/chart/{TICKER}?interval=1d&range=1d\n```\n8-second timeout, reads `.chart.result[0].meta.regularMarketPrice`.\n\n### UI Tabs (`stocks_crypto.php`)\n\n1. **Portfolio** — dashboard table with ticker, shares, cost basis, current value,\n   gain/loss, gain %, latest price date. Stats bar shows total value, total cost,\n   total gain/loss. \"Fetch Live Prices\" button calls Yahoo Finance for all tickers.\n\n2. **Enter Prices** — manual daily price entry grid for all active assets,\n   date picker (defaults to today), bulk save.\n\n3. **Crypto Buys** — monthly crypto purchase log. Month/year selector, table of\n   ticker/quantity/USD/notes rows, add and delete per entry.\n\n4. **Add Asset** — form to add a new stock, ETF, crypto, metal, or other asset.\n\n### Auto-Refresh\nA checkbox above the stats bar auto-refreshes the Portfolio tab every 10 seconds.\nState persists in `localStorage` key `sc_auto_refresh`. On reload, strips `?ok=`\nand `?warn=` query params to prevent the notice from repeating every 10 seconds.\n\n---\n\n## 7. Backend Home — New Cards (`index.php`)\n\nSix new cards added to the backend home dashboard:\n\n| Card | Link | Secondary Action |\n|------|------|-----------------|\n| Customer Totals | `/backend/customer_totals.php` | — |\n| Ambient Weather | `/backend/ambient_weather.php` | — |\n| Bills Manager | `/backend/bills.php` | — |\n| Stocks & Crypto | `/backend/stocks_crypto.php` | — |\n| Cron Manager | `/backend/cron_manager.php` | — |\n| Error Log | `/backend/error_log.php` | Change Log |\n\n---\n\n## 8. Documents Editor — Full Rewrite (`documents.php`)\n\n### Problem\nGitHub Copilot introduced several bugs into the previous version:\n- `docLoad()` had mismatched closing braces, breaking the entire function\n- The document library was constrained to ~40% page width inside a grid column\n- The editor was a plain `<textarea>` with no formatting support\n\n### Solution\nComplete rewrite of all HTML, CSS, and JavaScript. PHP POST handlers were kept\nidentical — only the front-end was replaced.\n\n### Editor: EasyMDE\nReplaced the plain textarea with **EasyMDE** (Easy Markdown Editor) via CDN:\n```html\n<link rel=\"stylesheet\" href=\"https://unpkg.com/easymde/dist/easymde.min.css\">\n<script src=\"https://unpkg.com/easymde/dist/easymde.min.js\"></script>\n```\n\nEasyMDE wraps the `<textarea id=\"content_markdown\">` and syncs content on form submit.\n`ta.value = easyMDE.value()` is called explicitly in `submitDoc()` before `form.submit()`.\n\n### Toolbar Buttons\nSave (custom gradient btn) | Bold · Italic · Strikethrough | H1 · H2 · H3 |\nUnordered List · Ordered List · Checklist (custom) | Quote · Code · Table |\nHR · Link · Image URL (custom) | Undo · Redo | Fullscreen | Guide\n\n- **Save** button in toolbar calls `submitDoc()`, same as the Save Document button below\n- **Ctrl+S / Cmd+S** keyboard shortcut via CodeMirror `extraKeys`\n- **Checklist** inserts `- [ ] ` prefix on selected text\n- **Image URL** prompts for URL and alt text, inserts `![alt](url)` syntax\n\n### Preview Toggle\nEasyMDE's built-in `preview` and `side-by-side` buttons were **removed** to prevent\nboth raw markdown and rendered HTML showing simultaneously.\n\nA custom **Preview** button in the panel header replaces them:\n- Uses `marked.js` (CDN) to render the markdown client-side\n- **Preview on**: hides `EasyMDEContainer` and highlight strip; shows `#doc-preview-pane`\n- **Edit on**: hides preview pane; shows EasyMDE and highlight strip; calls `codemirror.refresh()`\n- Loading a document via `docLoad()` automatically returns to edit mode\n- Button state: pill shape, switches between \"👁 Preview\" and \"✏ Edit\" labels\n\n```html\n<script src=\"https://cdn.jsdelivr.net/npm/marked/marked.min.js\"></script>\n```\n\n### `docLoad()` — Clean Rewrite\nOld function had mismatched braces and redundant `editor.value` assignments.\nNew function:\n1. Returns to edit mode if preview is on\n2. Sets heading to \"Loading…\"\n3. Highlights active `<tr>` and `<a>` in library immediately\n4. `fetch('/backend/api/documents.php?backend_document_id=' + id)`\n5. Populates all meta fields (type, title, slug, summary)\n6. Sets `easyMDE.value(content)` from `editor_content` or `content_markdown`\n7. Shows/hides file notice\n8. Calls `renderVersionList(data.versions)`\n9. Updates URL via `history.replaceState()`\n10. Flashes panel border with accent color for 800 ms\n\n### Library Panel — Full Width\nOld layout: library was inside a 2-column grid alongside the editor.\nNew layout:\n```\n[Editor Panel          — full width]\n[Library Panel         — full width, max-height: 460px scrollable]\n[Upload | Versions     — 2-column row]\n```\n\nLive client-side search filters table rows by `textContent` without a page reload.\n\n### Word Count & Highlights\n- Word count and char count update on every keystroke via `codemirror.on('change', ...)`\n- 6 highlight color swatches + a clear option wrap selected text in\n  `<span class=\"hl\" style=\"background:COLOR\">...</span>`\n- Clear highlight strips existing `<span class=\"hl\">` wrappers from selected text\n\n### CSS Theme Conformity (visit.html standard)\nAll custom CSS was updated to match the application theme:\n\n| Property | Before | After |\n|----------|--------|-------|\n| Form input border-radius | `10px` | `12px` (matches `--radius-sm`) |\n| Form input padding | `8px 10px` | `11px 14px` |\n| Field label font-size | `0.73rem` + uppercase | `0.82rem`, no forced uppercase |\n| Field label letter-spacing | `0.07em` | `0.04em` |\n| Focus ring | `outline: 2px solid var(--accent)` | `outline: 2px solid rgba(175,77,49,0.3)` |\n| Panel header background | `rgba(244,239,231,0.6)` hardcoded | `var(--panel)` |\n| Toolbar background | `rgba(244,239,231,0.7)` hardcoded | `rgba(255,255,255,0.55)` |\n| Action bar background | `rgba(244,239,231,0.4)` hardcoded | `rgba(255,255,255,0.25)` |\n| Save button | `background: var(--accent)` | `linear-gradient(135deg, var(--accent), var(--accent-deep))` |\n| Action buttons | `border-radius: 7px` | `border-radius: 999px` pill shape |\n| Active library row tint | `rgba(26,122,154,0.06)` teal | `rgba(175,77,49,0.07)` accent |\n| Type badge (markdown) | `rgba(26,122,154,0.1)` teal | `rgba(175,77,49,0.1)` accent |\n| File notice tint | `rgba(26,122,154,0.06)` teal | `rgba(175,77,49,0.05)` accent |\n\n---\n\n## 9. `document_service.php` — Parse Error Fix\n\n### Problem\nThe function `app_backend_documents_dir()` had its closing `return $path; }` accidentally\ndisplaced to after `app_document_can_edit_inline()` (lines 147–148), with the rest of the\nfile's functions (`app_libreoffice_convert_to_html`, etc.) nested inside the unclosed\nfunction body. This caused a PHP parse error on every request to any page that\n`require_once`'s `document_service.php`, making `app_document_can_edit_inline()`\nappear undefined.\n\n### Fix\nTwo edits to `document_service.php`:\n\n1. Closed `app_backend_documents_dir()` properly after the `mkdir` block:\n```php\nfunction app_backend_documents_dir(): string\n{\n    $path = __DIR__ . '/documents_storage';\n    if (!is_dir($path)) {\n        mkdir($path, 0775, true);\n    }\n    return $path;   // ← added\n}                   // ← added\n```\n\n2. Removed the orphaned `return $path; }` that had been displaced to after\n`app_document_can_edit_inline()` at lines 147–148.\n\n---\n\n## Access URLs\n\n| Module | URL |\n|--------|-----|\n| Backend Home | `/backend/` |\n| Bills Manager | `/backend/bills.php` |\n| Stocks & Crypto | `/backend/stocks_crypto.php` |\n| Cron Manager | `/backend/cron_manager.php` |\n| Error Log | `/backend/error_log.php` |\n| Change Log | `/backend/change_log.php` |\n| Documents | `/backend/documents.php` |\n| Documents API | `/backend/api/documents.php` |\n\n---\n\n## Technology Used\n\n| Library | Version | Purpose |\n|---------|---------|---------|\n| EasyMDE | latest (unpkg CDN) | Rich markdown editor |\n| marked.js | latest (jsdelivr CDN) | Client-side markdown → HTML rendering |\n| Yahoo Finance API | v8 (unofficial) | Live stock/ETF price fetching |\n| PHPMailer | (existing) | Email sending via `app_send_mail()` |\n\n---\n\n## Coding Conventions (consistent with existing codebase)\n\n- `declare(strict_types=1)` in all PHP files\n- `app_db()` for database connections, `mysqli` throughout\n- `app_query_all()`, `app_query_one()`, `app_h()`, `app_send_json()` helpers\n- `app_log_backend_exception()` for error logging\n- POST/Redirect/GET for all form submissions\n- `ON DUPLICATE KEY UPDATE` for safe upserts\n- `app_backend_render_header()` / `app_backend_render_footer()` for page chrome\n- CSS custom properties: `--accent`, `--panel`, `--line`, `--text`, `--muted`, `--shadow`\n- All new tables use `CREATE TABLE IF NOT EXISTS` (idempotent, no separate migration needed)\n","is_text_editable":1,"can_edit_inline":1}
← Prev 1 138 139 140