System
Audit trail of every save, delete, import, and workflow update across the backend.
1,029 results · Page 20 of 21
[]
{"visit_id":"129057","legacy_visit_id":null,"customer_id":"302528","visit_date":"2026-04-11 09:00:00","pickup_date":"2026-04-11 09:00:00","notes":"Wedding dress Deposit 50\nRange 180 ro 220","customer_name_snapshot":"Barbara Toledo Ramos","phone_snapshot":"8134033849","email_snapshot":"barbieb84@gmail.com","color":null,"alterations_needed":null,"alterations_notes":"{\"Project Details\":\"Wedding dress buttons \",\"When Needed?\":\"April 19th \"}","alterations_price":"0.00","total_alteration_price":"0.00","expedited_fee":"0.00","paid_date":null,"signature_text":null,"signature_date":null,"paid_flag":"0","source_key_value":"d93819b1-53f5-4f38-82f6-e2ba18e89b58","visit_type":"No label","deposit_amount":"0.00","setmore_customer_key":null}
{"visit_id":"129057","legacy_visit_id":null,"customer_id":"302528","visit_date":"2026-04-11 09:00:00","pickup_date":"2026-04-06 09:00:00","notes":"Wedding dress Deposit 50\r\nRange 180 ro 220","customer_name_snapshot":"Barbara Toledo Ramos","phone_snapshot":"8134033849","email_snapshot":"barbieb84@gmail.com","color":null,"alterations_needed":null,"alterations_notes":"{\"Project Details\":\"Wedding dress buttons \",\"When Needed?\":\"April 19th \"}","alterations_price":"0.00","total_alteration_price":"0.00","expedited_fee":"0.00","paid_date":null,"signature_text":null,"signature_date":null,"paid_flag":"0","source_key_value":"d93819b1-53f5-4f38-82f6-e2ba18e89b58","visit_type":"No label","deposit_amount":"0.00","setmore_customer_key":null}
{"file_name":"SESSION_LOG_2026-04-10.md","mime_type":"application/octet-stream"}
[]
{"backend_document_id":"13","document_type":"upload","title":"Codex - Updates 04-10-2026","slug":"codex-updates-04-10-2026","summary_text":"All updates and documentation for Codex - Updates 04-10-2026","content_markdown":null,"content_html":null,"file_name":"SESSION_LOG_2026-04-10.md","stored_name":"20260410-200855-4b6f846d.md","mime_type":"application/octet-stream","file_size_bytes":"9223","storage_path":"/mnt/drive1/customerdb/backend/documents_storage/20260410-200855-4b6f846d.md","is_deleted":"0","created_at":"2026-04-10 16:08:55","updated_at":"2026-04-10 16:08:55","editor_content":"# Session Log - 2026-04-10\n\nThis document captures the main fixes and modules completed during the live-data/go-live support session.\n\n## Customer Go-Live Data\n\n- Reviewed the live load path for `customers`, `visits`, `orders`, `order_items`, and skipped `payments` because the bridge source only had summary-style `CustomerTotals`, not reliable one-row-per-payment detail.\n- Confirmed the remaining unmatched Setmore/import rows were considered junk and safe to ignore.\n- Identified the only manual staging fixes needed before the production load:\n - fix `Priscilla`\n - fix `Rae Sanders`\n - ignore `Maria Martinez`\n- Prepared the go-live sequence:\n - backup current live tables\n - refresh staging\n - apply manual staging fixes\n - run preview script\n - run production load\n - run post-load audits\n - spot-check real customers in the app\n- Provided a backup-table SQL pattern for pre-go-live snapshots and later cleanup.\n\n## Visit Page\n\n- Added a safe staff override path so staff can leave `visit.html` without forcing a customer signature when the customer is not present.\n- Kept signature requirements for print/email/report actions, so the override only affects navigation.\n- Added a Bridge Compare popup/grid to `visit.html`.\n- Added backend endpoint for bridge comparison:\n - `frontend/api/bridge_customer_compare.php`\n- Updated the bridge comparison query to match by current customer name only:\n - `CustomerTable` joined to `CustomerVisits1` on `CustomerName`\n - `CustomerTable` joined to `AlterationItems` on `CustomerName`\n- Fixed bridge popup JSON failures caused by missing helper/bootstrap loading.\n- Fixed visit type persistence so saved `Pick Up` visits do not revert back to `Drop Off` unless a user changes it.\n- Fixed visit save/payment handling so the frontend payment array is respected instead of collapsing into a single payment row.\n- Fixed order totals so deposits plus payments reduce the due balance correctly.\n\n## Reports And Customer Totals\n\n- Fixed `reports.html` ready/customer results so paid totals calculate from live `orders` and `payments` by `visit_id`.\n- Fixed `customer.html` so paid totals are recomputed from live `payments`.\n- Fixed top totals snapshots to prefer live `visits`, `orders`, and `payments`, falling back to `customer_totals` only when no live data exists.\n- Updated `webui/api/totals_snapshot_live.php` so `webui.html` totals are also live-data based.\n- Added stronger visual separation between Today totals and Month-to-Date totals.\n- Added a `Completed` report filter.\n- Added per-record `Completed` badge logic.\n- Deduplicated report cards by `customer_id` to avoid duplicate customer cards such as Tabitha Torres.\n- Linked reports to new import paths:\n - Morning Import\n - Imported Today\n - End Of Day Review\n\n## End Of Day Review\n\n- Added the End Of Day Review module:\n - `backend/end_of_day_review.php`\n - `webui/backend/end_of_day_review.php`\n- The module summarizes:\n - daily closeout totals\n - appointments needing review\n - imported rows for the selected day\n - recent cron/job runs\n - no-shows\n - final status mix\n- Linked it from Backend Home and Reports.\n\n## Customer Emails And Reviews\n\n- Expanded the Customer Emails & Reviews workflow:\n - Send Email\n - Mark Sent\n - Ignore\n - outreach status tracking\n - delivery/result tracking\n - change-log entries\n- Updated files:\n - `backend/cust_email.php`\n - `backend/cust_email_service.php`\n - `webui/backend/cust_email.php`\n - `webui/backend/cust_email_service.php`\n- Updated the history view into an Outreach Log.\n\n## Email Hub Date And Time\n\n- Improved loaded appointment context handling in Email Hub.\n- Added editable appointment fields:\n - combined appointment date/time\n - appointment date\n - appointment time\n- Confirmation drafts and Setmore note updates now merge in the edited appointment fields.\n- Updated files:\n - `backend/email.php`\n - `webui/backend/email.php`\n\n## Cron Logging And Notifications\n\n- Added cron run logging through a shared runner.\n- Added recent run history to Cron Manager.\n- Added wrapper-based cron execution:\n - `backend/jobs/run_cron_job.php`\n - `webui/backend/jobs/run_cron_job.php`\n- Added shared runtime service:\n - `backend/jobs/cron_runtime_service.php`\n - `webui/backend/jobs/cron_runtime_service.php`\n- Updated cron export so scheduled jobs call the wrapper rather than raw commands.\n- Added notification plumbing for:\n - Telegram\n - email\n - SMS/Twilio\n- Added settings support for cron notification email.\n\n## Database Backup Module\n\n- Added a real Database Backup backend module.\n- It snapshots the major five production workflow tables:\n - `customers`\n - `visits`\n - `orders`\n - `order_items`\n - `payments`\n- Snapshots go into a separate database, default:\n - `ellas_alterations_backup`\n- Snapshot table names use suffixes like:\n - `customers__snapshot_YYYYMMDD_HHMMSS`\n- Snapshot history is logged in:\n - `backup_database_snapshot_log`\n- Added delete support for backup sets so backup tables do not grow without control.\n- Delete Set drops all backup tables for that snapshot and removes the matching log rows.\n- Updated files:\n - `backend/backup_database.php`\n - `backend/backup_database_service.php`\n - `webui/backend/backup_database.php`\n - `webui/backend/backup_database_service.php`\n\n## Live Table Manager\n\n- Added a guarded live table manager for direct maintenance of the five workflow tables:\n - `customers`\n - `visits`\n - `orders`\n - `order_items`\n - `payments`\n- Supports:\n - search\n - add\n - edit\n - delete\n- Every save/delete writes to the change log.\n- Added backup warning and quick link to Database Backup.\n- New files:\n - `backend/live_table_manager.php`\n - `backend/live_table_manager_service.php`\n - `webui/backend/live_table_manager.php`\n - `webui/backend/live_table_manager_service.php`\n- Linked from Backend Home.\n\n## Change Logging\n\n- Expanded visit-save logging so related entities are logged more clearly:\n - customer\n - visit\n - order\n - alterations/order items\n - payments\n- Added change logging for backup snapshot create/delete.\n- Added change logging for live table manager save/delete.\n\n## Edwards Mileage Split\n\n- Added an `Edwards Type` field for customer totals:\n - mileage\n - maintenance\n - other\n- Added separate Edwards totals in the drill view.\n- Updated schema installer to include `edwards_type`.\n- Updated:\n - `backend/customer_totals.php`\n - `backend/customer_totals_service.php`\n - `webui/backend/customer_totals.php`\n - `webui/backend/customer_totals_service.php`\n - `backend/install_schema.php`\n - `webui/backend/install_schema.php`\n\n## Setmore And Bridge Freshness\n\n- Added Setmore/bridge freshness module:\n - `backend/setmore_import_status.php`\n - `webui/backend/setmore_import_status.php`\n- Shows local import status and bridge table freshness for:\n - `ellas_access_bridge.CustomerTable`\n - `ellas_access_bridge.CustomerVisits1`\n - `ellas_access_bridge.AlterationItems`\n - `ellas_access_bridge.CustomerTotals`\n- Updated Setmore service so it prefers `app_settings` values before older config-file values.\n\n## Umami\n\n- Diagnosed iframe failure:\n - Umami sends restrictive frame/CSP headers.\n - HTTP iframe inside HTTPS backend is blocked as mixed content.\n- Built Ella-specific Umami source from `C:\\code\\umami`.\n- Initially tested a split setup:\n - app on `192.168.7.202`\n - database on `192.168.7.206`\n- Found repeated database connection failures on the split app:\n - `Connection terminated unexpectedly`\n- Moved the Ella Umami app to `192.168.7.206` so app and database run together.\n- Current direct login:\n - `http://192.168.7.206:3006/login`\n- Shut down the unstable `202` mirror app.\n- Updated backend Umami launcher pages:\n - `backend/umami.php`\n - `webui/backend/umami.php`\n- The iframe now targets the planned proxied path:\n - `/backend/umami-embed/login`\n- Nginx still needs the reverse proxy block for true HTTPS embedding.\n\n## WebUI Dashboard\n\n- Added `Current Speed Dial` to `webui.html` above the Finance card.\n- Changed Speed Dial on `webui.html` into a compact spreadsheet-style table so it uses less vertical space.\n- The section loads live data from:\n - `/backend/api/speed_dial.php`\n- Updated:\n - `webui/webui.html`\n\n## Medication Module\n\n- Fixed black/dark button styling in the Medication module by adding explicit theme-safe button styling.\n- Updated:\n - `backend/medication.php`\n - `webui/backend/medication.php`\n\n## Backend Navigation\n\n- Renamed Bills Manager to Pay Bills in visible backend UI.\n- Added links/cards for:\n - Database Backup\n - Live Table Manager\n - End Of Day Review\n - Setmore/Bridge Freshness\n\n## Remaining Follow-Up\n\n- Deploy/sync changed files to the live server.\n- Run the schema installer before using Edwards type live.\n- Test the Backup Delete Set button with a disposable snapshot.\n- Test Live Table Manager carefully after taking a backup snapshot.\n- Add the Nginx `/backend/umami-embed/` reverse proxy block before expecting Umami iframe embedding to work.\n- Verify Twilio credentials end-to-end.\n- Verify Email Hub date/time behavior on real imported emails.\n- Continue improving printable Daily Detail and Daily Appointment Operations layouts if needed.\n","is_text_editable":1,"can_edit_inline":1}
{"workflow_status":"no_show","visit_id":129050,"order_id":103803}
[]
{"workflow_id":"5","source":"visit","source_key":"129050","appointment_date":"2026-04-10","customer_id":"488994","visit_id":"129050","order_id":null,"workflow_status":"no_show","payment_state":null,"payment_method":null,"payment_amount":"0.00","paid_in_advance":"0","report_required":"0","report_attached":"0","report_checked_at":"2026-04-10 15:57:31","provider_notes_text":null,"operator_notes":"{\"Project Details\":\"Wedding dress\",\"When Needed?\":\"ASAP!! Will pay extra\"}","no_show_email_key":"none","created_at":"2026-04-10 15:57:31","updated_at":"2026-04-10 15:57:31"}
{"document_type":"markdown"}
{"backend_document_id":"12","document_type":"upload","title":"Square - Telegram IDS","slug":"square-telegram-ids","summary_text":"Square - Telegram IDS tokens and chat information","content_markdown":null,"content_html":null,"file_name":"SquareTelegramInfo.txt","stored_name":"20260408-160751-b757b5ec.txt","mime_type":"text/plain","file_size_bytes":"1716","storage_path":"/mnt/drive1/customerdb/backend/documents_storage/20260408-160751-b757b5ec.txt","is_deleted":"0","created_at":"2026-04-08 12:07:51","updated_at":"2026-04-08 12:07:51","editor_content":"The good news: everything is already built. No coding needed. Here's what you actually have to do:\r\n\r\nThe 6 steps:\r\nCreate a Square Developer Application at developer.squareup.com (free, ~2 min)\r\n\r\nCollect 3 credentials from the Square Developer dashboard:\r\n\r\nProduction Application ID (sq0idp-...)\r\nProduction Access Token (EAAAl...)\r\nLocation ID (L...)\r\nEnter in Settings page — the app's Settings screen has a Square Payments section. Put in the Access Token and Location ID there.\r\n\r\nFill in /frontend/api/config.php on the server — add all 3 credentials and change 'environment' => 'sandbox' to 'production'\r\n\r\nPair the Terminal device to your Square account/location via the device's own settings screen (if not already done)\r\n\r\nTest — open a visit, click \"Send to Terminal\", select the device, watch the Terminal screen light up\r\n\r\nThe document also includes the complete troubleshooting section for common issues (empty device list, auth failures, sandbox vs production mix-ups) and a quick-reference table showing which credentials go where.\r\n\r\n\r\nSandbox\r\nApplication ID\r\nsandbox-sq0idb-RADNkYqnTwtu2oXD-mJrHw\r\n\r\nAccess Token\r\nEAAAl7SwvhAwj63SPJuFnP0Bb2bbZQYwr_oLdiiXPprm3Yhf59YWfS6sQcTy7EVe\r\n\r\n\r\n\r\nLive\r\nApplication ID\r\nsq0idp-Ug5yEttIeIxgVk_FbfUXHw\r\n\r\nProduction Access Token\r\nEAAAlx3Bg1jm3Xa-qXGcy4BmucPfqmYTRqy1mISS5YeETWonr_4d8xSgnMU1_aQv\r\n\r\n\r\ndefault test account\r\nEAAAl7SwvhAwj63SPJuFnP0Bb2bbZQYwr_oLdiiXPprm3Yhf59YWfS6sQcTy7EVe\r\n\r\nSandbox\r\ntest Locations\r\nDefault Test Account (Main)\r\n1600 Pennsylvania Ave NW\r\nLQ9JH35T5GAYS\r\n7299\r\n\r\nProduction\r\nElla’s Alterations (Main)\r\n6986 Fort King Road\r\n5W9V4C95PWDVQ\r\n7230\r\n\r\n\r\ntelegram\r\n7700366760:AAEeLLs1XvbapLFx7biDSTC2quO8NzjfBac\r\n7990799442\r\n\r\n","is_text_editable":1,"can_edit_inline":1}
{"backend_document_id":"12","document_type":"markdown","title":"Change Of Business Hours","slug":"change-of-business-hours","summary_text":"This is all the areas to change for hour change","content_markdown":"# **Hours**\r\nSetmore\r\nNew Customerdb\r\nWebsite\r\nFacebook\r\nNextdoor\r\nYelp","content_html":"<h1>**Hours**</h1>\n<p>Setmore</p>\n<p>New Customerdb</p>\n<p>Website</p>\n<p>Facebook</p>\n<p>Nextdoor</p>\n<p>Yelp</p>","file_name":"SquareTelegramInfo.txt","stored_name":"20260408-160751-b757b5ec.txt","mime_type":"text/plain","file_size_bytes":"1716","storage_path":"/mnt/drive1/customerdb/backend/documents_storage/20260408-160751-b757b5ec.txt","is_deleted":"0","created_at":"2026-04-08 12:07:51","updated_at":"2026-04-10 13:20:34","editor_content":"# **Hours**\r\nSetmore\r\nNew Customerdb\r\nWebsite\r\nFacebook\r\nNextdoor\r\nYelp","is_text_editable":1,"can_edit_inline":1}
[]
[]
{"target_database":"ellas_alterations_backup","tables":[{"source_table":"customers","backup_table":"customers__snapshot_20260410_171516","row_count":3174},{"source_table":"visits","backup_table":"visits__snapshot_20260410_171516","row_count":979},{"source_table":"orders","backup_table":"orders__snapshot_20260410_171516","row_count":979},{"source_table":"order_items","backup_table":"order_items__snapshot_20260410_171516","row_count":90},{"source_table":"payments","backup_table":"payments__snapshot_20260410_171516","row_count":1260}]}
[]
[]
{"backend_speed_dial_id":"7","link_name":"New Umami","link_url":"http://192.168.7.206:3006/websites/6c928ae4-b073-4202-8b7d-35670995223f","sort_order":"0","is_active":"1","created_at":"2026-04-10 13:06:33","updated_at":"2026-04-10 13:06:33"}
[]
[]
{"backend_speed_dial_id":"6","link_name":"Technitium 143","link_url":"http://192.168.7.143:5380/","sort_order":"0","is_active":"1","created_at":"2026-04-09 09:48:34","updated_at":"2026-04-09 09:48:34"}
[]
[]
{"backend_speed_dial_id":"5","link_name":"Technitium 206","link_url":"http://192.168.7.206:5380/","sort_order":"0","is_active":"1","created_at":"2026-04-09 09:48:17","updated_at":"2026-04-09 09:48:17"}
[]
[]
{"backend_speed_dial_id":"4","link_name":"N8N 143","link_url":"http://192.168.7.143:5678/home/workflows","sort_order":"0","is_active":"1","created_at":"2026-04-09 09:47:35","updated_at":"2026-04-09 09:47:35"}
{"file_name":"SquareTelegramInfo.txt","mime_type":"text/plain"}
[]
{"backend_document_id":"12","document_type":"upload","title":"Square - Telegram IDS","slug":"square-telegram-ids","summary_text":"Square - Telegram IDS tokens and chat information","content_markdown":null,"content_html":null,"file_name":"SquareTelegramInfo.txt","stored_name":"20260408-160751-b757b5ec.txt","mime_type":"text/plain","file_size_bytes":"1716","storage_path":"/mnt/drive1/customerdb/backend/documents_storage/20260408-160751-b757b5ec.txt","is_deleted":"0","created_at":"2026-04-08 12:07:51","updated_at":"2026-04-08 12:07:51","editor_content":"The good news: everything is already built. No coding needed. Here's what you actually have to do:\r\n\r\nThe 6 steps:\r\nCreate a Square Developer Application at developer.squareup.com (free, ~2 min)\r\n\r\nCollect 3 credentials from the Square Developer dashboard:\r\n\r\nProduction Application ID (sq0idp-...)\r\nProduction Access Token (EAAAl...)\r\nLocation ID (L...)\r\nEnter in Settings page — the app's Settings screen has a Square Payments section. Put in the Access Token and Location ID there.\r\n\r\nFill in /frontend/api/config.php on the server — add all 3 credentials and change 'environment' => 'sandbox' to 'production'\r\n\r\nPair the Terminal device to your Square account/location via the device's own settings screen (if not already done)\r\n\r\nTest — open a visit, click \"Send to Terminal\", select the device, watch the Terminal screen light up\r\n\r\nThe document also includes the complete troubleshooting section for common issues (empty device list, auth failures, sandbox vs production mix-ups) and a quick-reference table showing which credentials go where.\r\n\r\n\r\nSandbox\r\nApplication ID\r\nsandbox-sq0idb-RADNkYqnTwtu2oXD-mJrHw\r\n\r\nAccess Token\r\nEAAAl7SwvhAwj63SPJuFnP0Bb2bbZQYwr_oLdiiXPprm3Yhf59YWfS6sQcTy7EVe\r\n\r\n\r\n\r\nLive\r\nApplication ID\r\nsq0idp-Ug5yEttIeIxgVk_FbfUXHw\r\n\r\nProduction Access Token\r\nEAAAlx3Bg1jm3Xa-qXGcy4BmucPfqmYTRqy1mISS5YeETWonr_4d8xSgnMU1_aQv\r\n\r\n\r\ndefault test account\r\nEAAAl7SwvhAwj63SPJuFnP0Bb2bbZQYwr_oLdiiXPprm3Yhf59YWfS6sQcTy7EVe\r\n\r\nSandbox\r\ntest Locations\r\nDefault Test Account (Main)\r\n1600 Pennsylvania Ave NW\r\nLQ9JH35T5GAYS\r\n7299\r\n\r\nProduction\r\nElla’s Alterations (Main)\r\n6986 Fort King Road\r\n5W9V4C95PWDVQ\r\n7230\r\n\r\n\r\ntelegram\r\n7700366760:AAEeLLs1XvbapLFx7biDSTC2quO8NzjfBac\r\n7990799442\r\n\r\n","is_text_editable":1,"can_edit_inline":1}
{"file_name":"SESSION_LOG_2026-04-07.md","mime_type":"application/octet-stream"}
[]
{"backend_document_id":"11","document_type":"upload","title":"Codex 04-07-2026 Documentation","slug":"codex-04-07-2026-documentation","summary_text":"Codex 04-07-2026 Documentation everything that has been completed today","content_markdown":null,"content_html":null,"file_name":"SESSION_LOG_2026-04-07.md","stored_name":"20260407-191340-bcefed15.md","mime_type":"application/octet-stream","file_size_bytes":"12740","storage_path":"/mnt/drive1/customerdb/backend/documents_storage/20260407-191340-bcefed15.md","is_deleted":"0","created_at":"2026-04-07 15:13:40","updated_at":"2026-04-07 15:13:40","editor_content":"# Session Log - 2026-04-07\n\n## Summary\n\nThis document records the work completed on `2026-04-07` for the `customerdb` system across:\n\n- frontend / front desk\n- webui / management\n- backend modules\n- reporting\n- signatures / Topaz\n- Square Terminal\n- documents editor / theming\n\nIt is meant to be the end-of-day handoff and operator reference used before quitting for the day.\n\n---\n\n## Main Outcomes\n\nThe main work completed today focused on:\n\n1. stabilizing visit save, payment, and signature behavior\n2. improving printed reports and work orders\n3. improving visit activity logging and history\n4. fixing queue/report visibility by visit instead of collapsing by customer\n5. building and pairing the Square Terminal API flow\n6. continuing backend theme conformity, especially the Documents module\n\n---\n\n## Completed Work\n\n## 1. Front Desk Visit Workflow\n\nFiles touched:\n\n- [visit.html](/C:/code/customerdb/frontend/visit.html)\n- [visit_save.php](/C:/code/customerdb/frontend/api/visit_save.php)\n- [visit_get.php](/C:/code/customerdb/frontend/api/visit_get.php)\n\n### 1.1 Topaz Signature Flow\n\nCompleted:\n\n- Topaz signature capture now auto-saves the visit immediately after a successful capture.\n- Receipt/work order/report actions require a signature for normal visit types.\n- Leaving the visit page prompts/blocks when a required signature is missing.\n- Report generation first saves the visit if the form is dirty, helping ensure the current signature is included.\n\nWhat this fixes:\n\n- signature captured on screen but not yet saved to the visit\n- report opened too quickly before signature save completed\n- missing signature on receipt/PDF after successful Topaz signing\n\n### 1.2 Visit Activity Log\n\nCompleted:\n\n- Added per-visit activity logging to the visit save pipeline.\n- Added display of activity history at the bottom of the visit form.\n- Log now records:\n - timestamp\n - actor\n - activity type\n - changed field names\n - before values\n - after values\n- Friendly field labels were added for log display.\n\nExamples of friendly labels:\n\n- Pickup Date\n- Deposit Amount\n- Amount Paid\n- Payment Method\n- Tip Amount\n- Total Before Tax\n- Total After Tax\n\n### 1.3 Square Metadata on Visit\n\nCompleted:\n\n- Added Square metadata capture on the visit form and save pipeline:\n - Square payment ID\n - Square receipt / confirmation number\n - card brand\n - last 4\n - checkout ID\n - Square tip amount\n - Square total amount\n- Added a visible `Square Reference` area on the visit page.\n- Added Square payment activity log entries.\n\n### 1.4 Payment and Save Logic\n\nCompleted:\n\n- Removed the redundant `No Tax (order)` checkbox from `Payment & Status`.\n- `Picked Up` logic was previously tightened so visit type should drive status rather than auto-forcing pickup.\n- Added a payment line log under `Payment & Status`.\n- Visit save now logs changes and payment events more clearly.\n\nKnown caution:\n\n- Continue live testing around edge-case payment edits involving deposits, split payments, and later pickups.\n\n---\n\n## 2. Printed Reports and Customer-Facing Reports\n\nFiles touched:\n\n- [receipt.html](/C:/code/customerdb/frontend/receipt.html)\n- [work_order.html](/C:/code/customerdb/frontend/work_order.html)\n- [customer_display.html](/C:/code/customerdb/frontend/customer_display.html)\n\n### 2.1 Receipt\n\nCompleted:\n\n- Added `Pickup Time`.\n- Added `Status`.\n- Receipt now uses normalized visit status, such as:\n - Drop Off\n - Pick Up\n - Canceled\n - No Show\n- Receipt shows pickup date/time more clearly.\n\n### 2.2 Customer Display\n\nCompleted:\n\n- Added `Pickup Time` to the customer-facing display.\n\n### 2.3 Work Order\n\nCompleted:\n\n- Added:\n - Pickup Time\n - Status\n - Total Before Tax\n - Tax\n - Total After Tax\n - Deposit\n - Balance\n- Explicit placeholders such as `$0.00` are shown when values are absent so staff can tell the difference between blank and zero.\n- Added `Visit History` to the work order.\n- Then refined `Visit History` to be concise and single-line only so the work order stays print-friendly.\n\nCurrent work-order history behavior:\n\n- only shows status changes and payment-type events\n- examples:\n - `Status Changed: Drop Off to Pick Up`\n - `Payment: Deposit · Cash · 40.00`\n - `Square Payment 85.00 · Confirmation # 123456`\n\nReason for this change:\n\n- the earlier detailed before/after layout was too large for the print page\n\n---\n\n## 3. Queue / Reports Fixes\n\nFiles touched:\n\n- [customer_data.php](/C:/code/customerdb/frontend/api/customer_data.php)\n- [customer_data.php](/C:/code/customerdb/webui/api/customer_data.php)\n\n### 3.1 Today / Ready Queue Logic\n\nCompleted:\n\n- Fixed queue grouping so operational lists now behave per visit rather than collapsing multiple same-customer visits into one row.\n- Fixed display names so queue views use `visits.customer_name_snapshot` when appropriate.\n\nThis fixed real cases where these visits were being merged or displayed incorrectly:\n\n- Peter Dixon\n- Kefa Dixon\n- Angela Dixon\n- Me Myexperttailor\n\n### 3.2 Dashboard and Ella Side Consistency\n\nCompleted:\n\n- `dashboard.floridaalterations.com/reports.html?type=ready` now shows separate visits correctly.\n- `ella.floridaalterations.com/reports.html?type=today` now mirrors the same corrected behavior.\n\n---\n\n## 4. Square Terminal Integration\n\nFiles touched:\n\n- [square_terminal.php](/C:/code/customerdb/frontend/api/square_terminal.php)\n- [square_terminal.php](/C:/code/customerdb/backend/square_terminal.php)\n- [square_terminal_service.php](/C:/code/customerdb/backend/square_terminal_service.php)\n- [settings.html](/C:/code/customerdb/webui/settings.html)\n- [settings_get.php](/C:/code/customerdb/webui/api/settings_get.php)\n- [settings_save.php](/C:/code/customerdb/webui/api/settings_save.php)\n\n### 4.1 What Was Built\n\nCompleted:\n\n- built a backend Square Terminal pairing page/module\n- added support for:\n - create device code\n - refresh code status\n - sync devices\n - store paired devices in MariaDB\n- added manual terminal device ID / label support in settings\n\nTables added earlier in this work:\n\n- `backend_square_terminal_code`\n- `backend_square_terminal_device`\n\n### 4.2 Pairing Result\n\nCompleted:\n\n- real terminal pairing succeeded\n- paired device returned by Square:\n - `device:343CS149B6001234`\n- terminal name:\n - `Ella`\n- device status:\n - `AVAILABLE`\n\n### 4.3 Visit Integration\n\nCompleted:\n\n- visit page can now talk to Square Terminal API using the configured device\n- checkout completion returns:\n - payment ID\n - base amount\n - total amount\n - tip amount\n - card brand\n - last 4\n - receipt number / confirmation number\n\nKnown caution:\n\n- continue live validation that tip handling exactly matches Square on every real terminal transaction\n\n---\n\n## 5. Backend Documents Module\n\nFiles touched:\n\n- [documents.php](/C:/code/customerdb/backend/documents.php)\n\n### 5.1 Functional State\n\nThe Documents backend already supported:\n\n- markdown editing\n- upload\n- document library\n- history / versions\n- restore version\n\n### 5.2 Theme / Color Work Completed Today\n\nCompleted:\n\n- extensive theme cleanup for Documents page\n- fixed dark/black areas in:\n - editor panel\n - library panel\n - upload panel\n - history panel\n - version list area\n - toolbar save button\n - save document button\n- aligned notices, buttons, inputs, editor chrome, headings, and versions to the backend theme\n\nFinal problem areas specifically addressed today:\n\n- `.doc-panel-head`\n- `.doc-panel-head-left`\n- `.doc-bottom-panel`\n- `.doc-lib-body`\n- `#versionList`\n- toolbar `Save`\n- form `Save Document`\n\nStatus at end of day:\n\n- user confirmed it is much better after the final passes\n\n---\n\n## 6. Work Order / Activity History Printing\n\nFiles touched:\n\n- [work_order.html](/C:/code/customerdb/frontend/work_order.html)\n\nCompleted:\n\n- condensed the history so it fits better on one page\n- removed verbose multi-row history in favor of concise operational lines\n\nUse case:\n\n- person working the garment can see what happened on the ticket without the sheet becoming too large to print\n\n---\n\n## How To Use - Daily Workflow\n\n## Morning\n\n1. Run Setmore import.\n2. Open today’s clients / ready queues and review all current visits.\n3. Open visits as needed and verify:\n - visit type\n - payment/deposit values\n - signature status\n - report status\n4. If using Square Terminal:\n - ensure the paired device is still available in backend Square Terminal page\n - run one small test only if needed\n\n## During the Day\n\n1. Open visit.\n2. Update alteration items, visit type, payment, or deposit as needed.\n3. Capture signature with Topaz.\n4. The visit should auto-save immediately after Topaz capture.\n5. Print/open:\n - receipt\n - customer display\n - work order\n6. Review activity log at bottom of visit if something looks off.\n\n## End of Day\n\n1. Review reports and operational queues.\n2. Check printed work orders / receipts for correct status and payment details.\n3. Review Documents page theme/behavior only if that module was used.\n4. If Square was used, confirm confirmation numbers and payment activity are present in the visit log.\n\n---\n\n## How To Test\n\n## A. Topaz / Report Test\n\nUse a real visit:\n\n1. Open visit.\n2. Capture signature with Topaz.\n3. Confirm the page auto-saves.\n4. Open receipt.\n5. Confirm signature is already present.\n\nExpected:\n\n- no extra manual save needed after successful Topaz signature\n\n## B. Visit Activity Log Test\n\n1. Open visit.\n2. Change:\n - pickup date\n - payment amount\n - visit type\n3. Save.\n4. Review bottom `Visit Activity Log`.\n\nExpected:\n\n- friendlier field labels\n- before/after values visible\n\n## C. Work Order Test\n\n1. Open work order.\n2. Verify it shows:\n - pickup date and time\n - status\n - total before tax\n - tax\n - total after tax\n - deposit\n - balance\n3. Check `Visit History`.\n\nExpected:\n\n- only concise status/payment entries\n- should be more likely to fit on one page\n\n## D. Queue Test\n\n1. Open dashboard ready page.\n2. Open Ella today page.\n3. Confirm each visit is shown separately.\n\nExpected:\n\n- no merging of same-customer visits\n\n## E. Square Terminal Test\n\n1. Open backend Square Terminal page.\n2. Confirm device status is available.\n3. Open a visit.\n4. Send amount to terminal.\n5. Complete payment.\n6. Verify visit page shows:\n - Square confirmation number\n - card info\n - payment metadata\n\nExpected:\n\n- payment returns to visit with matching Square identifiers\n\n## F. Documents Theme Test\n\n1. Open Documents page.\n2. Check:\n - editor header\n - library panel\n - upload panel\n - history panel\n - version list\n - save buttons\n\nExpected:\n\n- no black panels/buttons\n- same backend color family as other backend pages\n\n---\n\n## What Was Verified\n\nVerified during implementation:\n\n- local PHP lint on changed PHP files\n- remote PHP lint on deployed files\n- multiple live pages returning 200 during deployment\n- real Square Terminal pairing status reached `PAIRED`\n- Square device became available to the app\n\nUser-confirmed / observed improvements:\n\n- Documents page looked much better after later theme passes\n\n---\n\n## Open Items / Follow-Up\n\nThese are the main items still worth continued live testing, not necessarily known broken items:\n\n1. Square tip logic in real-world transactions\n2. payment edge cases involving deposit, later balance payment, and visit type changes\n3. printed report fit on one page for very large work orders\n4. any remaining Documents CSS drift if another black element appears\n\n---\n\n## Recommended First Tests Tomorrow\n\n1. Test one real visit from start to finish:\n - edit visit\n - Topaz sign\n - auto-save\n - receipt\n - work order\n2. Test one real Square Terminal payment.\n3. Check today/ready queue counts on both dashboard and Ella sides.\n4. Open Documents and verify the final themed state in normal browser and incognito.\n\n---\n\n## Key Pages\n\nFrontend:\n\n- `https://dashboard.floridaalterations.com/visit.html?visit_id=...&customer_id=...`\n- `https://dashboard.floridaalterations.com/receipt.html?visit_id=...`\n- `https://dashboard.floridaalterations.com/work_order.html?visit_id=...`\n- `https://dashboard.floridaalterations.com/reports.html?type=ready`\n\nManagement / Ella:\n\n- `https://ella.floridaalterations.com/reports.html?type=today`\n- `https://ella.floridaalterations.com/backend/documents.php`\n- `https://ella.floridaalterations.com/backend/square_terminal.php`\n\n---\n\n## Final Note\n\nToday’s work pushed the system closer to dependable daily operations in three major areas:\n\n- the visit/signature/report flow\n- the per-visit operational queue/report logic\n- the Square Terminal integration path\n\nThe biggest operational win of the day is that Topaz signature capture now writes back immediately, which should reduce missed signatures on receipts and reports.\n","is_text_editable":1,"can_edit_inline":1}
{"document_type":"upload"}
{"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 `` 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}
{"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 `` 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 & 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'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 "Balance After" 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's appointments.</p>\n<h3>Email Content</h3>\n<ul>\n<li>Subject: `Reminder: Your appointment at Ella'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 "Run Now" |</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> *` → "Daily at 20:00"</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'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 `<details>` 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 & 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 = ''): 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 = 'manual'): 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&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. "Fetch Live Prices" 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 & 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 `<textarea>` 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><link rel="stylesheet" href="https://unpkg.com/easymde/dist/easymde.min.css"></p>\n<p><script src="https://unpkg.com/easymde/dist/easymde.min.js"></script></p>\n<p>```</p>\n<p>EasyMDE wraps the `<textarea id="content_markdown">` 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 `` syntax</li>\n</ul>\n<h3>Preview Toggle</h3>\n<p>EasyMDE'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 "👁 Preview" and "✏ Edit" labels</li>\n</ul>\n<p>```html</p>\n<p><script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script></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 "Loading…"</p>\n<p>3. Highlights active `<tr>` and `<a>` in library immediately</p>\n<p>4. `fetch('/backend/api/documents.php?backend_document_id=' + 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 & Highlights</h3>\n<ul>\n<li>Word count and char count update on every keystroke via `codemirror.on('change', ...)`</li>\n<li>6 highlight color swatches + a clear option wrap selected text in</li>\n</ul>\n<p>`<span class="hl" style="background:COLOR">...</span>`</p>\n<ul>\n<li>Clear highlight strips existing `<span class="hl">` 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'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`'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__ . '/documents_storage';</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 & 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 `` 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}
{"file_name":"SESSION_LOG_2026-04-07.md","mime_type":"application/octet-stream"}
[]
{"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 `` 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}
[]
{"backend_square_terminal_code_id":"43","square_device_code_id":"DTMM1Q9V2N4DE","code_value":"YPAMEA","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"UNPAIRED","location_id":"5W9V4C95PWDVQ","pair_by":"2026-04-07 16:49:34","paired_at":"2026-04-07 16:44:34","raw_json":"{\"id\":\"DTMM1Q9V2N4DE\",\"name\":\"Front Desk Terminal\",\"code\":\"YPAMEA\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"pair_by\":\"2026-04-07T16:49:34.000Z\",\"created_at\":\"2026-04-07T16:44:34.226Z\",\"status\":\"UNPAIRED\",\"status_changed_at\":\"2026-04-07T16:44:34.000Z\"}","created_at":"2026-04-07 12:44:34","updated_at":"2026-04-07 12:44:34"}
{"backend_square_terminal_code_id":"43","square_device_code_id":"DTMM1Q9V2N4DE","code_value":"YPAMEA","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"PAIRED","location_id":"5W9V4C95PWDVQ","pair_by":null,"paired_at":"2026-04-07 16:44:53","raw_json":"{\"id\":\"DTMM1Q9V2N4DE\",\"name\":\"Front Desk Terminal\",\"code\":\"YPAMEA\",\"device_id\":\"343CS149B6001234\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"created_at\":\"2026-04-07T16:44:34.000Z\",\"status\":\"PAIRED\",\"status_changed_at\":\"2026-04-07T16:44:53.000Z\",\"paired_at\":\"2026-04-07T16:44:53.000Z\"}","created_at":"2026-04-07 12:44:34","updated_at":"2026-04-07 12:44:59"}
[]
[]
{"backend_square_terminal_code_id":"43","square_device_code_id":"DTMM1Q9V2N4DE","code_value":"YPAMEA","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"UNPAIRED","location_id":"5W9V4C95PWDVQ","pair_by":"2026-04-07 16:49:34","paired_at":"2026-04-07 16:44:34","raw_json":"{\"id\":\"DTMM1Q9V2N4DE\",\"name\":\"Front Desk Terminal\",\"code\":\"YPAMEA\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"pair_by\":\"2026-04-07T16:49:34.000Z\",\"created_at\":\"2026-04-07T16:44:34.226Z\",\"status\":\"UNPAIRED\",\"status_changed_at\":\"2026-04-07T16:44:34.000Z\"}","created_at":"2026-04-07 12:44:34","updated_at":"2026-04-07 12:44:34"}
[]
{"backend_square_terminal_code_id":"21","square_device_code_id":"PMCKQJJ4CRN5W","code_value":"FTRVTD","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"UNPAIRED","location_id":"5W9V4C95PWDVQ","pair_by":"2026-04-07 16:34:01","paired_at":"2026-04-07 16:29:01","raw_json":"{\"id\":\"PMCKQJJ4CRN5W\",\"name\":\"Front Desk Terminal\",\"code\":\"FTRVTD\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"pair_by\":\"2026-04-07T16:34:01.000Z\",\"created_at\":\"2026-04-07T16:29:02.000Z\",\"status\":\"UNPAIRED\",\"status_changed_at\":\"2026-04-07T16:29:01.000Z\"}","created_at":"2026-04-07 12:29:02","updated_at":"2026-04-07 12:32:39"}
{"backend_square_terminal_code_id":"21","square_device_code_id":"PMCKQJJ4CRN5W","code_value":"FTRVTD","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"UNPAIRED","location_id":"5W9V4C95PWDVQ","pair_by":"2026-04-07 16:34:01","paired_at":"2026-04-07 16:29:01","raw_json":"{\"id\":\"PMCKQJJ4CRN5W\",\"name\":\"Front Desk Terminal\",\"code\":\"FTRVTD\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"pair_by\":\"2026-04-07T16:34:01.000Z\",\"created_at\":\"2026-04-07T16:29:02.000Z\",\"status\":\"UNPAIRED\",\"status_changed_at\":\"2026-04-07T16:29:01.000Z\"}","created_at":"2026-04-07 12:29:02","updated_at":"2026-04-07 12:32:47"}
[]
{"backend_square_terminal_code_id":"1","square_device_code_id":"5JVYM58J39RMV","code_value":"NJQDJK","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"EXPIRED","location_id":"5W9V4C95PWDVQ","pair_by":null,"paired_at":"1970-01-01 00:00:00","raw_json":"{\"id\":\"5JVYM58J39RMV\",\"name\":\"Front Desk Terminal\",\"code\":\"NJQDJK\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"created_at\":\"2026-04-07T16:00:25.000Z\",\"status\":\"EXPIRED\",\"status_changed_at\":\"1970-01-01T00:00:00.000Z\"}","created_at":"2026-04-07 12:00:24","updated_at":"2026-04-07 12:09:16"}
{"backend_square_terminal_code_id":"1","square_device_code_id":"5JVYM58J39RMV","code_value":"NJQDJK","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"EXPIRED","location_id":"5W9V4C95PWDVQ","pair_by":null,"paired_at":"1970-01-01 00:00:00","raw_json":"{\"id\":\"5JVYM58J39RMV\",\"name\":\"Front Desk Terminal\",\"code\":\"NJQDJK\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"created_at\":\"2026-04-07T16:00:25.000Z\",\"status\":\"EXPIRED\",\"status_changed_at\":\"1970-01-01T00:00:00.000Z\"}","created_at":"2026-04-07 12:00:24","updated_at":"2026-04-07 12:32:40"}
[]
{"backend_square_terminal_code_id":"9","square_device_code_id":"7M14JAR5FMGAR","code_value":"HCWMAB","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"EXPIRED","location_id":"5W9V4C95PWDVQ","pair_by":null,"paired_at":"1970-01-01 00:00:00","raw_json":"{\"id\":\"7M14JAR5FMGAR\",\"name\":\"Front Desk Terminal\",\"code\":\"HCWMAB\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"created_at\":\"2026-04-07T16:10:16.000Z\",\"status\":\"EXPIRED\",\"status_changed_at\":\"1970-01-01T00:00:00.000Z\"}","created_at":"2026-04-07 12:10:16","updated_at":"2026-04-07 12:22:56"}
{"backend_square_terminal_code_id":"9","square_device_code_id":"7M14JAR5FMGAR","code_value":"HCWMAB","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"EXPIRED","location_id":"5W9V4C95PWDVQ","pair_by":null,"paired_at":"1970-01-01 00:00:00","raw_json":"{\"id\":\"7M14JAR5FMGAR\",\"name\":\"Front Desk Terminal\",\"code\":\"HCWMAB\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"created_at\":\"2026-04-07T16:10:16.000Z\",\"status\":\"EXPIRED\",\"status_changed_at\":\"1970-01-01T00:00:00.000Z\"}","created_at":"2026-04-07 12:10:16","updated_at":"2026-04-07 12:32:40"}
[]
{"backend_square_terminal_code_id":"14","square_device_code_id":"J2DTPYA05PXJ8","code_value":"SVDRSM","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"EXPIRED","location_id":"5W9V4C95PWDVQ","pair_by":null,"paired_at":"1970-01-01 00:00:00","raw_json":"{\"id\":\"J2DTPYA05PXJ8\",\"name\":\"Front Desk Terminal\",\"code\":\"SVDRSM\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"created_at\":\"2026-04-07T16:23:06.000Z\",\"status\":\"EXPIRED\",\"status_changed_at\":\"1970-01-01T00:00:00.000Z\"}","created_at":"2026-04-07 12:23:06","updated_at":"2026-04-07 12:28:57"}
{"backend_square_terminal_code_id":"14","square_device_code_id":"J2DTPYA05PXJ8","code_value":"SVDRSM","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"EXPIRED","location_id":"5W9V4C95PWDVQ","pair_by":null,"paired_at":"1970-01-01 00:00:00","raw_json":"{\"id\":\"J2DTPYA05PXJ8\",\"name\":\"Front Desk Terminal\",\"code\":\"SVDRSM\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"created_at\":\"2026-04-07T16:23:06.000Z\",\"status\":\"EXPIRED\",\"status_changed_at\":\"1970-01-01T00:00:00.000Z\"}","created_at":"2026-04-07 12:23:06","updated_at":"2026-04-07 12:32:39"}
[]
{"backend_square_terminal_code_id":"21","square_device_code_id":"PMCKQJJ4CRN5W","code_value":"FTRVTD","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"UNPAIRED","location_id":"5W9V4C95PWDVQ","pair_by":"2026-04-07 16:34:01","paired_at":"2026-04-07 16:29:01","raw_json":"{\"id\":\"PMCKQJJ4CRN5W\",\"name\":\"Front Desk Terminal\",\"code\":\"FTRVTD\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"pair_by\":\"2026-04-07T16:34:01.000Z\",\"created_at\":\"2026-04-07T16:29:02.000Z\",\"status\":\"UNPAIRED\",\"status_changed_at\":\"2026-04-07T16:29:01.000Z\"}","created_at":"2026-04-07 12:29:02","updated_at":"2026-04-07 12:32:31"}
{"backend_square_terminal_code_id":"21","square_device_code_id":"PMCKQJJ4CRN5W","code_value":"FTRVTD","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"UNPAIRED","location_id":"5W9V4C95PWDVQ","pair_by":"2026-04-07 16:34:01","paired_at":"2026-04-07 16:29:01","raw_json":"{\"id\":\"PMCKQJJ4CRN5W\",\"name\":\"Front Desk Terminal\",\"code\":\"FTRVTD\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"pair_by\":\"2026-04-07T16:34:01.000Z\",\"created_at\":\"2026-04-07T16:29:02.000Z\",\"status\":\"UNPAIRED\",\"status_changed_at\":\"2026-04-07T16:29:01.000Z\"}","created_at":"2026-04-07 12:29:02","updated_at":"2026-04-07 12:32:39"}
[]
{"backend_square_terminal_code_id":"21","square_device_code_id":"PMCKQJJ4CRN5W","code_value":"FTRVTD","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"UNPAIRED","location_id":"5W9V4C95PWDVQ","pair_by":"2026-04-07 16:34:01","paired_at":"2026-04-07 16:29:01","raw_json":"{\"id\":\"PMCKQJJ4CRN5W\",\"name\":\"Front Desk Terminal\",\"code\":\"FTRVTD\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"pair_by\":\"2026-04-07T16:34:01.000Z\",\"created_at\":\"2026-04-07T16:29:02.000Z\",\"status\":\"UNPAIRED\",\"status_changed_at\":\"2026-04-07T16:29:01.000Z\"}","created_at":"2026-04-07 12:29:02","updated_at":"2026-04-07 12:32:27"}
{"backend_square_terminal_code_id":"21","square_device_code_id":"PMCKQJJ4CRN5W","code_value":"FTRVTD","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"UNPAIRED","location_id":"5W9V4C95PWDVQ","pair_by":"2026-04-07 16:34:01","paired_at":"2026-04-07 16:29:01","raw_json":"{\"id\":\"PMCKQJJ4CRN5W\",\"name\":\"Front Desk Terminal\",\"code\":\"FTRVTD\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"pair_by\":\"2026-04-07T16:34:01.000Z\",\"created_at\":\"2026-04-07T16:29:02.000Z\",\"status\":\"UNPAIRED\",\"status_changed_at\":\"2026-04-07T16:29:01.000Z\"}","created_at":"2026-04-07 12:29:02","updated_at":"2026-04-07 12:32:31"}
[]
{"backend_square_terminal_code_id":"21","square_device_code_id":"PMCKQJJ4CRN5W","code_value":"FTRVTD","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"UNPAIRED","location_id":"5W9V4C95PWDVQ","pair_by":"2026-04-07 16:34:01","paired_at":"2026-04-07 16:29:01","raw_json":"{\"id\":\"PMCKQJJ4CRN5W\",\"name\":\"Front Desk Terminal\",\"code\":\"FTRVTD\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"pair_by\":\"2026-04-07T16:34:01.000Z\",\"created_at\":\"2026-04-07T16:29:02.000Z\",\"status\":\"UNPAIRED\",\"status_changed_at\":\"2026-04-07T16:29:01.000Z\"}","created_at":"2026-04-07 12:29:02","updated_at":"2026-04-07 12:32:22"}
{"backend_square_terminal_code_id":"21","square_device_code_id":"PMCKQJJ4CRN5W","code_value":"FTRVTD","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"UNPAIRED","location_id":"5W9V4C95PWDVQ","pair_by":"2026-04-07 16:34:01","paired_at":"2026-04-07 16:29:01","raw_json":"{\"id\":\"PMCKQJJ4CRN5W\",\"name\":\"Front Desk Terminal\",\"code\":\"FTRVTD\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"pair_by\":\"2026-04-07T16:34:01.000Z\",\"created_at\":\"2026-04-07T16:29:02.000Z\",\"status\":\"UNPAIRED\",\"status_changed_at\":\"2026-04-07T16:29:01.000Z\"}","created_at":"2026-04-07 12:29:02","updated_at":"2026-04-07 12:32:27"}
[]
{"backend_square_terminal_code_id":"21","square_device_code_id":"PMCKQJJ4CRN5W","code_value":"FTRVTD","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"UNPAIRED","location_id":"5W9V4C95PWDVQ","pair_by":"2026-04-07 16:34:01","paired_at":"2026-04-07 16:29:01","raw_json":"{\"id\":\"PMCKQJJ4CRN5W\",\"name\":\"Front Desk Terminal\",\"code\":\"FTRVTD\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"pair_by\":\"2026-04-07T16:34:01.000Z\",\"created_at\":\"2026-04-07T16:29:02.000Z\",\"status\":\"UNPAIRED\",\"status_changed_at\":\"2026-04-07T16:29:01.000Z\"}","created_at":"2026-04-07 12:29:02","updated_at":"2026-04-07 12:30:56"}
{"backend_square_terminal_code_id":"21","square_device_code_id":"PMCKQJJ4CRN5W","code_value":"FTRVTD","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"UNPAIRED","location_id":"5W9V4C95PWDVQ","pair_by":"2026-04-07 16:34:01","paired_at":"2026-04-07 16:29:01","raw_json":"{\"id\":\"PMCKQJJ4CRN5W\",\"name\":\"Front Desk Terminal\",\"code\":\"FTRVTD\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"pair_by\":\"2026-04-07T16:34:01.000Z\",\"created_at\":\"2026-04-07T16:29:02.000Z\",\"status\":\"UNPAIRED\",\"status_changed_at\":\"2026-04-07T16:29:01.000Z\"}","created_at":"2026-04-07 12:29:02","updated_at":"2026-04-07 12:32:22"}
[]
{"backend_square_terminal_code_id":"21","square_device_code_id":"PMCKQJJ4CRN5W","code_value":"FTRVTD","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"UNPAIRED","location_id":"5W9V4C95PWDVQ","pair_by":"2026-04-07 16:34:01","paired_at":"2026-04-07 16:29:01","raw_json":"{\"id\":\"PMCKQJJ4CRN5W\",\"name\":\"Front Desk Terminal\",\"code\":\"FTRVTD\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"pair_by\":\"2026-04-07T16:34:01.000Z\",\"created_at\":\"2026-04-07T16:29:02.000Z\",\"status\":\"UNPAIRED\",\"status_changed_at\":\"2026-04-07T16:29:01.000Z\"}","created_at":"2026-04-07 12:29:02","updated_at":"2026-04-07 12:30:53"}
{"backend_square_terminal_code_id":"21","square_device_code_id":"PMCKQJJ4CRN5W","code_value":"FTRVTD","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"UNPAIRED","location_id":"5W9V4C95PWDVQ","pair_by":"2026-04-07 16:34:01","paired_at":"2026-04-07 16:29:01","raw_json":"{\"id\":\"PMCKQJJ4CRN5W\",\"name\":\"Front Desk Terminal\",\"code\":\"FTRVTD\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"pair_by\":\"2026-04-07T16:34:01.000Z\",\"created_at\":\"2026-04-07T16:29:02.000Z\",\"status\":\"UNPAIRED\",\"status_changed_at\":\"2026-04-07T16:29:01.000Z\"}","created_at":"2026-04-07 12:29:02","updated_at":"2026-04-07 12:30:56"}
[]
{"backend_square_terminal_code_id":"21","square_device_code_id":"PMCKQJJ4CRN5W","code_value":"FTRVTD","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"UNPAIRED","location_id":"5W9V4C95PWDVQ","pair_by":"2026-04-07 16:34:01","paired_at":"2026-04-07 16:29:01","raw_json":"{\"id\":\"PMCKQJJ4CRN5W\",\"name\":\"Front Desk Terminal\",\"code\":\"FTRVTD\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"pair_by\":\"2026-04-07T16:34:01.000Z\",\"created_at\":\"2026-04-07T16:29:02.000Z\",\"status\":\"UNPAIRED\",\"status_changed_at\":\"2026-04-07T16:29:01.000Z\"}","created_at":"2026-04-07 12:29:02","updated_at":"2026-04-07 12:30:50"}
{"backend_square_terminal_code_id":"21","square_device_code_id":"PMCKQJJ4CRN5W","code_value":"FTRVTD","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"UNPAIRED","location_id":"5W9V4C95PWDVQ","pair_by":"2026-04-07 16:34:01","paired_at":"2026-04-07 16:29:01","raw_json":"{\"id\":\"PMCKQJJ4CRN5W\",\"name\":\"Front Desk Terminal\",\"code\":\"FTRVTD\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"pair_by\":\"2026-04-07T16:34:01.000Z\",\"created_at\":\"2026-04-07T16:29:02.000Z\",\"status\":\"UNPAIRED\",\"status_changed_at\":\"2026-04-07T16:29:01.000Z\"}","created_at":"2026-04-07 12:29:02","updated_at":"2026-04-07 12:30:53"}
[]
{"backend_square_terminal_code_id":"21","square_device_code_id":"PMCKQJJ4CRN5W","code_value":"FTRVTD","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"UNPAIRED","location_id":"5W9V4C95PWDVQ","pair_by":"2026-04-07 16:34:01","paired_at":"2026-04-07 16:29:01","raw_json":"{\"id\":\"PMCKQJJ4CRN5W\",\"name\":\"Front Desk Terminal\",\"code\":\"FTRVTD\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"pair_by\":\"2026-04-07T16:34:01.000Z\",\"created_at\":\"2026-04-07T16:29:02.000Z\",\"status\":\"UNPAIRED\",\"status_changed_at\":\"2026-04-07T16:29:01.000Z\"}","created_at":"2026-04-07 12:29:02","updated_at":"2026-04-07 12:30:46"}
{"backend_square_terminal_code_id":"21","square_device_code_id":"PMCKQJJ4CRN5W","code_value":"FTRVTD","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"UNPAIRED","location_id":"5W9V4C95PWDVQ","pair_by":"2026-04-07 16:34:01","paired_at":"2026-04-07 16:29:01","raw_json":"{\"id\":\"PMCKQJJ4CRN5W\",\"name\":\"Front Desk Terminal\",\"code\":\"FTRVTD\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"pair_by\":\"2026-04-07T16:34:01.000Z\",\"created_at\":\"2026-04-07T16:29:02.000Z\",\"status\":\"UNPAIRED\",\"status_changed_at\":\"2026-04-07T16:29:01.000Z\"}","created_at":"2026-04-07 12:29:02","updated_at":"2026-04-07 12:30:50"}
[]
{"backend_square_terminal_code_id":"21","square_device_code_id":"PMCKQJJ4CRN5W","code_value":"FTRVTD","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"UNPAIRED","location_id":"5W9V4C95PWDVQ","pair_by":"2026-04-07 16:34:01","paired_at":"2026-04-07 16:29:01","raw_json":"{\"id\":\"PMCKQJJ4CRN5W\",\"name\":\"Front Desk Terminal\",\"code\":\"FTRVTD\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"pair_by\":\"2026-04-07T16:34:01.000Z\",\"created_at\":\"2026-04-07T16:29:02.000Z\",\"status\":\"UNPAIRED\",\"status_changed_at\":\"2026-04-07T16:29:01.000Z\"}","created_at":"2026-04-07 12:29:02","updated_at":"2026-04-07 12:30:37"}
{"backend_square_terminal_code_id":"21","square_device_code_id":"PMCKQJJ4CRN5W","code_value":"FTRVTD","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"UNPAIRED","location_id":"5W9V4C95PWDVQ","pair_by":"2026-04-07 16:34:01","paired_at":"2026-04-07 16:29:01","raw_json":"{\"id\":\"PMCKQJJ4CRN5W\",\"name\":\"Front Desk Terminal\",\"code\":\"FTRVTD\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"pair_by\":\"2026-04-07T16:34:01.000Z\",\"created_at\":\"2026-04-07T16:29:02.000Z\",\"status\":\"UNPAIRED\",\"status_changed_at\":\"2026-04-07T16:29:01.000Z\"}","created_at":"2026-04-07 12:29:02","updated_at":"2026-04-07 12:30:46"}
[]
{"backend_square_terminal_code_id":"21","square_device_code_id":"PMCKQJJ4CRN5W","code_value":"FTRVTD","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"UNPAIRED","location_id":"5W9V4C95PWDVQ","pair_by":"2026-04-07 16:34:01","paired_at":"2026-04-07 16:29:01","raw_json":"{\"id\":\"PMCKQJJ4CRN5W\",\"name\":\"Front Desk Terminal\",\"code\":\"FTRVTD\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"pair_by\":\"2026-04-07T16:34:01.000Z\",\"created_at\":\"2026-04-07T16:29:02.000Z\",\"status\":\"UNPAIRED\",\"status_changed_at\":\"2026-04-07T16:29:01.000Z\"}","created_at":"2026-04-07 12:29:02","updated_at":"2026-04-07 12:30:34"}
{"backend_square_terminal_code_id":"21","square_device_code_id":"PMCKQJJ4CRN5W","code_value":"FTRVTD","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"UNPAIRED","location_id":"5W9V4C95PWDVQ","pair_by":"2026-04-07 16:34:01","paired_at":"2026-04-07 16:29:01","raw_json":"{\"id\":\"PMCKQJJ4CRN5W\",\"name\":\"Front Desk Terminal\",\"code\":\"FTRVTD\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"pair_by\":\"2026-04-07T16:34:01.000Z\",\"created_at\":\"2026-04-07T16:29:02.000Z\",\"status\":\"UNPAIRED\",\"status_changed_at\":\"2026-04-07T16:29:01.000Z\"}","created_at":"2026-04-07 12:29:02","updated_at":"2026-04-07 12:30:37"}
[]
{"backend_square_terminal_code_id":"21","square_device_code_id":"PMCKQJJ4CRN5W","code_value":"FTRVTD","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"UNPAIRED","location_id":"5W9V4C95PWDVQ","pair_by":"2026-04-07 16:34:01","paired_at":"2026-04-07 16:29:01","raw_json":"{\"id\":\"PMCKQJJ4CRN5W\",\"name\":\"Front Desk Terminal\",\"code\":\"FTRVTD\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"pair_by\":\"2026-04-07T16:34:01.000Z\",\"created_at\":\"2026-04-07T16:29:02.000Z\",\"status\":\"UNPAIRED\",\"status_changed_at\":\"2026-04-07T16:29:01.000Z\"}","created_at":"2026-04-07 12:29:02","updated_at":"2026-04-07 12:30:30"}
{"backend_square_terminal_code_id":"21","square_device_code_id":"PMCKQJJ4CRN5W","code_value":"FTRVTD","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"UNPAIRED","location_id":"5W9V4C95PWDVQ","pair_by":"2026-04-07 16:34:01","paired_at":"2026-04-07 16:29:01","raw_json":"{\"id\":\"PMCKQJJ4CRN5W\",\"name\":\"Front Desk Terminal\",\"code\":\"FTRVTD\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"pair_by\":\"2026-04-07T16:34:01.000Z\",\"created_at\":\"2026-04-07T16:29:02.000Z\",\"status\":\"UNPAIRED\",\"status_changed_at\":\"2026-04-07T16:29:01.000Z\"}","created_at":"2026-04-07 12:29:02","updated_at":"2026-04-07 12:30:34"}
[]
{"backend_square_terminal_code_id":"21","square_device_code_id":"PMCKQJJ4CRN5W","code_value":"FTRVTD","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"UNPAIRED","location_id":"5W9V4C95PWDVQ","pair_by":"2026-04-07 16:34:01","paired_at":"2026-04-07 16:29:01","raw_json":"{\"id\":\"PMCKQJJ4CRN5W\",\"name\":\"Front Desk Terminal\",\"code\":\"FTRVTD\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"pair_by\":\"2026-04-07T16:34:01.000Z\",\"created_at\":\"2026-04-07T16:29:02.000Z\",\"status\":\"UNPAIRED\",\"status_changed_at\":\"2026-04-07T16:29:01.000Z\"}","created_at":"2026-04-07 12:29:02","updated_at":"2026-04-07 12:30:19"}
{"backend_square_terminal_code_id":"21","square_device_code_id":"PMCKQJJ4CRN5W","code_value":"FTRVTD","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"UNPAIRED","location_id":"5W9V4C95PWDVQ","pair_by":"2026-04-07 16:34:01","paired_at":"2026-04-07 16:29:01","raw_json":"{\"id\":\"PMCKQJJ4CRN5W\",\"name\":\"Front Desk Terminal\",\"code\":\"FTRVTD\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"pair_by\":\"2026-04-07T16:34:01.000Z\",\"created_at\":\"2026-04-07T16:29:02.000Z\",\"status\":\"UNPAIRED\",\"status_changed_at\":\"2026-04-07T16:29:01.000Z\"}","created_at":"2026-04-07 12:29:02","updated_at":"2026-04-07 12:30:30"}
[]
{"backend_square_terminal_code_id":"21","square_device_code_id":"PMCKQJJ4CRN5W","code_value":"FTRVTD","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"UNPAIRED","location_id":"5W9V4C95PWDVQ","pair_by":"2026-04-07 16:34:01","paired_at":"2026-04-07 16:29:01","raw_json":"{\"id\":\"PMCKQJJ4CRN5W\",\"name\":\"Front Desk Terminal\",\"code\":\"FTRVTD\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"pair_by\":\"2026-04-07T16:34:01.000Z\",\"created_at\":\"2026-04-07T16:29:02.000Z\",\"status\":\"UNPAIRED\",\"status_changed_at\":\"2026-04-07T16:29:01.000Z\"}","created_at":"2026-04-07 12:29:02","updated_at":"2026-04-07 12:30:14"}
{"backend_square_terminal_code_id":"21","square_device_code_id":"PMCKQJJ4CRN5W","code_value":"FTRVTD","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"UNPAIRED","location_id":"5W9V4C95PWDVQ","pair_by":"2026-04-07 16:34:01","paired_at":"2026-04-07 16:29:01","raw_json":"{\"id\":\"PMCKQJJ4CRN5W\",\"name\":\"Front Desk Terminal\",\"code\":\"FTRVTD\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"pair_by\":\"2026-04-07T16:34:01.000Z\",\"created_at\":\"2026-04-07T16:29:02.000Z\",\"status\":\"UNPAIRED\",\"status_changed_at\":\"2026-04-07T16:29:01.000Z\"}","created_at":"2026-04-07 12:29:02","updated_at":"2026-04-07 12:30:19"}
[]
{"backend_square_terminal_code_id":"21","square_device_code_id":"PMCKQJJ4CRN5W","code_value":"FTRVTD","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"UNPAIRED","location_id":"5W9V4C95PWDVQ","pair_by":"2026-04-07 16:34:01","paired_at":"2026-04-07 16:29:01","raw_json":"{\"id\":\"PMCKQJJ4CRN5W\",\"name\":\"Front Desk Terminal\",\"code\":\"FTRVTD\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"pair_by\":\"2026-04-07T16:34:01.000Z\",\"created_at\":\"2026-04-07T16:29:02.000Z\",\"status\":\"UNPAIRED\",\"status_changed_at\":\"2026-04-07T16:29:01.000Z\"}","created_at":"2026-04-07 12:29:02","updated_at":"2026-04-07 12:30:11"}
{"backend_square_terminal_code_id":"21","square_device_code_id":"PMCKQJJ4CRN5W","code_value":"FTRVTD","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"UNPAIRED","location_id":"5W9V4C95PWDVQ","pair_by":"2026-04-07 16:34:01","paired_at":"2026-04-07 16:29:01","raw_json":"{\"id\":\"PMCKQJJ4CRN5W\",\"name\":\"Front Desk Terminal\",\"code\":\"FTRVTD\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"pair_by\":\"2026-04-07T16:34:01.000Z\",\"created_at\":\"2026-04-07T16:29:02.000Z\",\"status\":\"UNPAIRED\",\"status_changed_at\":\"2026-04-07T16:29:01.000Z\"}","created_at":"2026-04-07 12:29:02","updated_at":"2026-04-07 12:30:14"}
[]
{"backend_square_terminal_code_id":"21","square_device_code_id":"PMCKQJJ4CRN5W","code_value":"FTRVTD","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"UNPAIRED","location_id":"5W9V4C95PWDVQ","pair_by":"2026-04-07 16:34:01","paired_at":"2026-04-07 16:29:01","raw_json":"{\"id\":\"PMCKQJJ4CRN5W\",\"name\":\"Front Desk Terminal\",\"code\":\"FTRVTD\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"pair_by\":\"2026-04-07T16:34:01.000Z\",\"created_at\":\"2026-04-07T16:29:02.000Z\",\"status\":\"UNPAIRED\",\"status_changed_at\":\"2026-04-07T16:29:01.000Z\"}","created_at":"2026-04-07 12:29:02","updated_at":"2026-04-07 12:30:06"}
{"backend_square_terminal_code_id":"21","square_device_code_id":"PMCKQJJ4CRN5W","code_value":"FTRVTD","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"UNPAIRED","location_id":"5W9V4C95PWDVQ","pair_by":"2026-04-07 16:34:01","paired_at":"2026-04-07 16:29:01","raw_json":"{\"id\":\"PMCKQJJ4CRN5W\",\"name\":\"Front Desk Terminal\",\"code\":\"FTRVTD\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"pair_by\":\"2026-04-07T16:34:01.000Z\",\"created_at\":\"2026-04-07T16:29:02.000Z\",\"status\":\"UNPAIRED\",\"status_changed_at\":\"2026-04-07T16:29:01.000Z\"}","created_at":"2026-04-07 12:29:02","updated_at":"2026-04-07 12:30:11"}
[]
{"backend_square_terminal_code_id":"21","square_device_code_id":"PMCKQJJ4CRN5W","code_value":"FTRVTD","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"UNPAIRED","location_id":"5W9V4C95PWDVQ","pair_by":"2026-04-07 16:34:01","paired_at":"2026-04-07 16:29:01","raw_json":"{\"id\":\"PMCKQJJ4CRN5W\",\"name\":\"Front Desk Terminal\",\"code\":\"FTRVTD\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"pair_by\":\"2026-04-07T16:34:01.000Z\",\"created_at\":\"2026-04-07T16:29:02.000Z\",\"status\":\"UNPAIRED\",\"status_changed_at\":\"2026-04-07T16:29:01.000Z\"}","created_at":"2026-04-07 12:29:02","updated_at":"2026-04-07 12:29:58"}
{"backend_square_terminal_code_id":"21","square_device_code_id":"PMCKQJJ4CRN5W","code_value":"FTRVTD","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"UNPAIRED","location_id":"5W9V4C95PWDVQ","pair_by":"2026-04-07 16:34:01","paired_at":"2026-04-07 16:29:01","raw_json":"{\"id\":\"PMCKQJJ4CRN5W\",\"name\":\"Front Desk Terminal\",\"code\":\"FTRVTD\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"pair_by\":\"2026-04-07T16:34:01.000Z\",\"created_at\":\"2026-04-07T16:29:02.000Z\",\"status\":\"UNPAIRED\",\"status_changed_at\":\"2026-04-07T16:29:01.000Z\"}","created_at":"2026-04-07 12:29:02","updated_at":"2026-04-07 12:30:06"}
[]
{"backend_square_terminal_code_id":"21","square_device_code_id":"PMCKQJJ4CRN5W","code_value":"FTRVTD","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"UNPAIRED","location_id":"5W9V4C95PWDVQ","pair_by":"2026-04-07 16:34:01","paired_at":"2026-04-07 16:29:01","raw_json":"{\"id\":\"PMCKQJJ4CRN5W\",\"name\":\"Front Desk Terminal\",\"code\":\"FTRVTD\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"pair_by\":\"2026-04-07T16:34:01.000Z\",\"created_at\":\"2026-04-07T16:29:02.000Z\",\"status\":\"UNPAIRED\",\"status_changed_at\":\"2026-04-07T16:29:01.000Z\"}","created_at":"2026-04-07 12:29:02","updated_at":"2026-04-07 12:29:26"}
{"backend_square_terminal_code_id":"21","square_device_code_id":"PMCKQJJ4CRN5W","code_value":"FTRVTD","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"UNPAIRED","location_id":"5W9V4C95PWDVQ","pair_by":"2026-04-07 16:34:01","paired_at":"2026-04-07 16:29:01","raw_json":"{\"id\":\"PMCKQJJ4CRN5W\",\"name\":\"Front Desk Terminal\",\"code\":\"FTRVTD\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"pair_by\":\"2026-04-07T16:34:01.000Z\",\"created_at\":\"2026-04-07T16:29:02.000Z\",\"status\":\"UNPAIRED\",\"status_changed_at\":\"2026-04-07T16:29:01.000Z\"}","created_at":"2026-04-07 12:29:02","updated_at":"2026-04-07 12:29:58"}
[]
{"backend_square_terminal_code_id":"21","square_device_code_id":"PMCKQJJ4CRN5W","code_value":"FTRVTD","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"UNPAIRED","location_id":"5W9V4C95PWDVQ","pair_by":"2026-04-07 16:34:01","paired_at":"2026-04-07 16:29:01","raw_json":"{\"id\":\"PMCKQJJ4CRN5W\",\"name\":\"Front Desk Terminal\",\"code\":\"FTRVTD\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"pair_by\":\"2026-04-07T16:34:01.000Z\",\"created_at\":\"2026-04-07T16:29:01.854Z\",\"status\":\"UNPAIRED\",\"status_changed_at\":\"2026-04-07T16:29:01.000Z\"}","created_at":"2026-04-07 12:29:02","updated_at":"2026-04-07 12:29:02"}
{"backend_square_terminal_code_id":"21","square_device_code_id":"PMCKQJJ4CRN5W","code_value":"FTRVTD","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"UNPAIRED","location_id":"5W9V4C95PWDVQ","pair_by":"2026-04-07 16:34:01","paired_at":"2026-04-07 16:29:01","raw_json":"{\"id\":\"PMCKQJJ4CRN5W\",\"name\":\"Front Desk Terminal\",\"code\":\"FTRVTD\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"pair_by\":\"2026-04-07T16:34:01.000Z\",\"created_at\":\"2026-04-07T16:29:02.000Z\",\"status\":\"UNPAIRED\",\"status_changed_at\":\"2026-04-07T16:29:01.000Z\"}","created_at":"2026-04-07 12:29:02","updated_at":"2026-04-07 12:29:26"}
[]
[]
{"backend_square_terminal_code_id":"21","square_device_code_id":"PMCKQJJ4CRN5W","code_value":"FTRVTD","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"UNPAIRED","location_id":"5W9V4C95PWDVQ","pair_by":"2026-04-07 16:34:01","paired_at":"2026-04-07 16:29:01","raw_json":"{\"id\":\"PMCKQJJ4CRN5W\",\"name\":\"Front Desk Terminal\",\"code\":\"FTRVTD\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"pair_by\":\"2026-04-07T16:34:01.000Z\",\"created_at\":\"2026-04-07T16:29:01.854Z\",\"status\":\"UNPAIRED\",\"status_changed_at\":\"2026-04-07T16:29:01.000Z\"}","created_at":"2026-04-07 12:29:02","updated_at":"2026-04-07 12:29:02"}
[]
{"backend_square_terminal_code_id":"14","square_device_code_id":"J2DTPYA05PXJ8","code_value":"SVDRSM","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"EXPIRED","location_id":"5W9V4C95PWDVQ","pair_by":null,"paired_at":"1970-01-01 00:00:00","raw_json":"{\"id\":\"J2DTPYA05PXJ8\",\"name\":\"Front Desk Terminal\",\"code\":\"SVDRSM\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"created_at\":\"2026-04-07T16:23:06.000Z\",\"status\":\"EXPIRED\",\"status_changed_at\":\"1970-01-01T00:00:00.000Z\"}","created_at":"2026-04-07 12:23:06","updated_at":"2026-04-07 12:28:35"}
{"backend_square_terminal_code_id":"14","square_device_code_id":"J2DTPYA05PXJ8","code_value":"SVDRSM","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"EXPIRED","location_id":"5W9V4C95PWDVQ","pair_by":null,"paired_at":"1970-01-01 00:00:00","raw_json":"{\"id\":\"J2DTPYA05PXJ8\",\"name\":\"Front Desk Terminal\",\"code\":\"SVDRSM\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"created_at\":\"2026-04-07T16:23:06.000Z\",\"status\":\"EXPIRED\",\"status_changed_at\":\"1970-01-01T00:00:00.000Z\"}","created_at":"2026-04-07 12:23:06","updated_at":"2026-04-07 12:28:57"}
[]
{"backend_square_terminal_code_id":"14","square_device_code_id":"J2DTPYA05PXJ8","code_value":"SVDRSM","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"UNPAIRED","location_id":"5W9V4C95PWDVQ","pair_by":"2026-04-07 16:28:06","paired_at":"2026-04-07 16:23:06","raw_json":"{\"id\":\"J2DTPYA05PXJ8\",\"name\":\"Front Desk Terminal\",\"code\":\"SVDRSM\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"pair_by\":\"2026-04-07T16:28:06.000Z\",\"created_at\":\"2026-04-07T16:23:06.000Z\",\"status\":\"UNPAIRED\",\"status_changed_at\":\"2026-04-07T16:23:06.000Z\"}","created_at":"2026-04-07 12:23:06","updated_at":"2026-04-07 12:27:47"}
{"backend_square_terminal_code_id":"14","square_device_code_id":"J2DTPYA05PXJ8","code_value":"SVDRSM","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"EXPIRED","location_id":"5W9V4C95PWDVQ","pair_by":null,"paired_at":"1970-01-01 00:00:00","raw_json":"{\"id\":\"J2DTPYA05PXJ8\",\"name\":\"Front Desk Terminal\",\"code\":\"SVDRSM\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"created_at\":\"2026-04-07T16:23:06.000Z\",\"status\":\"EXPIRED\",\"status_changed_at\":\"1970-01-01T00:00:00.000Z\"}","created_at":"2026-04-07 12:23:06","updated_at":"2026-04-07 12:28:35"}
[]
{"backend_square_terminal_code_id":"14","square_device_code_id":"J2DTPYA05PXJ8","code_value":"SVDRSM","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"UNPAIRED","location_id":"5W9V4C95PWDVQ","pair_by":"2026-04-07 16:28:06","paired_at":"2026-04-07 16:23:06","raw_json":"{\"id\":\"J2DTPYA05PXJ8\",\"name\":\"Front Desk Terminal\",\"code\":\"SVDRSM\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"pair_by\":\"2026-04-07T16:28:06.000Z\",\"created_at\":\"2026-04-07T16:23:06.000Z\",\"status\":\"UNPAIRED\",\"status_changed_at\":\"2026-04-07T16:23:06.000Z\"}","created_at":"2026-04-07 12:23:06","updated_at":"2026-04-07 12:27:43"}
{"backend_square_terminal_code_id":"14","square_device_code_id":"J2DTPYA05PXJ8","code_value":"SVDRSM","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"UNPAIRED","location_id":"5W9V4C95PWDVQ","pair_by":"2026-04-07 16:28:06","paired_at":"2026-04-07 16:23:06","raw_json":"{\"id\":\"J2DTPYA05PXJ8\",\"name\":\"Front Desk Terminal\",\"code\":\"SVDRSM\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"pair_by\":\"2026-04-07T16:28:06.000Z\",\"created_at\":\"2026-04-07T16:23:06.000Z\",\"status\":\"UNPAIRED\",\"status_changed_at\":\"2026-04-07T16:23:06.000Z\"}","created_at":"2026-04-07 12:23:06","updated_at":"2026-04-07 12:27:47"}
[]
{"backend_square_terminal_code_id":"14","square_device_code_id":"J2DTPYA05PXJ8","code_value":"SVDRSM","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"UNPAIRED","location_id":"5W9V4C95PWDVQ","pair_by":"2026-04-07 16:28:06","paired_at":"2026-04-07 16:23:06","raw_json":"{\"id\":\"J2DTPYA05PXJ8\",\"name\":\"Front Desk Terminal\",\"code\":\"SVDRSM\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"pair_by\":\"2026-04-07T16:28:06.000Z\",\"created_at\":\"2026-04-07T16:23:06.000Z\",\"status\":\"UNPAIRED\",\"status_changed_at\":\"2026-04-07T16:23:06.000Z\"}","created_at":"2026-04-07 12:23:06","updated_at":"2026-04-07 12:27:38"}
{"backend_square_terminal_code_id":"14","square_device_code_id":"J2DTPYA05PXJ8","code_value":"SVDRSM","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"UNPAIRED","location_id":"5W9V4C95PWDVQ","pair_by":"2026-04-07 16:28:06","paired_at":"2026-04-07 16:23:06","raw_json":"{\"id\":\"J2DTPYA05PXJ8\",\"name\":\"Front Desk Terminal\",\"code\":\"SVDRSM\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"pair_by\":\"2026-04-07T16:28:06.000Z\",\"created_at\":\"2026-04-07T16:23:06.000Z\",\"status\":\"UNPAIRED\",\"status_changed_at\":\"2026-04-07T16:23:06.000Z\"}","created_at":"2026-04-07 12:23:06","updated_at":"2026-04-07 12:27:43"}
[]
{"backend_square_terminal_code_id":"14","square_device_code_id":"J2DTPYA05PXJ8","code_value":"SVDRSM","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"UNPAIRED","location_id":"5W9V4C95PWDVQ","pair_by":"2026-04-07 16:28:06","paired_at":"2026-04-07 16:23:06","raw_json":"{\"id\":\"J2DTPYA05PXJ8\",\"name\":\"Front Desk Terminal\",\"code\":\"SVDRSM\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"pair_by\":\"2026-04-07T16:28:06.000Z\",\"created_at\":\"2026-04-07T16:23:06.000Z\",\"status\":\"UNPAIRED\",\"status_changed_at\":\"2026-04-07T16:23:06.000Z\"}","created_at":"2026-04-07 12:23:06","updated_at":"2026-04-07 12:27:22"}
{"backend_square_terminal_code_id":"14","square_device_code_id":"J2DTPYA05PXJ8","code_value":"SVDRSM","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"UNPAIRED","location_id":"5W9V4C95PWDVQ","pair_by":"2026-04-07 16:28:06","paired_at":"2026-04-07 16:23:06","raw_json":"{\"id\":\"J2DTPYA05PXJ8\",\"name\":\"Front Desk Terminal\",\"code\":\"SVDRSM\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"pair_by\":\"2026-04-07T16:28:06.000Z\",\"created_at\":\"2026-04-07T16:23:06.000Z\",\"status\":\"UNPAIRED\",\"status_changed_at\":\"2026-04-07T16:23:06.000Z\"}","created_at":"2026-04-07 12:23:06","updated_at":"2026-04-07 12:27:38"}
[]
{"backend_square_terminal_code_id":"14","square_device_code_id":"J2DTPYA05PXJ8","code_value":"SVDRSM","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"UNPAIRED","location_id":"5W9V4C95PWDVQ","pair_by":"2026-04-07 16:28:06","paired_at":"2026-04-07 16:23:06","raw_json":"{\"id\":\"J2DTPYA05PXJ8\",\"name\":\"Front Desk Terminal\",\"code\":\"SVDRSM\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"pair_by\":\"2026-04-07T16:28:06.000Z\",\"created_at\":\"2026-04-07T16:23:06.476Z\",\"status\":\"UNPAIRED\",\"status_changed_at\":\"2026-04-07T16:23:06.000Z\"}","created_at":"2026-04-07 12:23:06","updated_at":"2026-04-07 12:23:06"}
{"backend_square_terminal_code_id":"14","square_device_code_id":"J2DTPYA05PXJ8","code_value":"SVDRSM","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"UNPAIRED","location_id":"5W9V4C95PWDVQ","pair_by":"2026-04-07 16:28:06","paired_at":"2026-04-07 16:23:06","raw_json":"{\"id\":\"J2DTPYA05PXJ8\",\"name\":\"Front Desk Terminal\",\"code\":\"SVDRSM\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"pair_by\":\"2026-04-07T16:28:06.000Z\",\"created_at\":\"2026-04-07T16:23:06.000Z\",\"status\":\"UNPAIRED\",\"status_changed_at\":\"2026-04-07T16:23:06.000Z\"}","created_at":"2026-04-07 12:23:06","updated_at":"2026-04-07 12:27:22"}
[]
[]
{"backend_square_terminal_code_id":"14","square_device_code_id":"J2DTPYA05PXJ8","code_value":"SVDRSM","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"UNPAIRED","location_id":"5W9V4C95PWDVQ","pair_by":"2026-04-07 16:28:06","paired_at":"2026-04-07 16:23:06","raw_json":"{\"id\":\"J2DTPYA05PXJ8\",\"name\":\"Front Desk Terminal\",\"code\":\"SVDRSM\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"pair_by\":\"2026-04-07T16:28:06.000Z\",\"created_at\":\"2026-04-07T16:23:06.476Z\",\"status\":\"UNPAIRED\",\"status_changed_at\":\"2026-04-07T16:23:06.000Z\"}","created_at":"2026-04-07 12:23:06","updated_at":"2026-04-07 12:23:06"}
[]
{"backend_square_terminal_code_id":"9","square_device_code_id":"7M14JAR5FMGAR","code_value":"HCWMAB","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"UNPAIRED","location_id":"5W9V4C95PWDVQ","pair_by":"2026-04-07 16:15:16","paired_at":"2026-04-07 16:10:16","raw_json":"{\"id\":\"7M14JAR5FMGAR\",\"name\":\"Front Desk Terminal\",\"code\":\"HCWMAB\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"pair_by\":\"2026-04-07T16:15:16.000Z\",\"created_at\":\"2026-04-07T16:10:16.000Z\",\"status\":\"UNPAIRED\",\"status_changed_at\":\"2026-04-07T16:10:16.000Z\"}","created_at":"2026-04-07 12:10:16","updated_at":"2026-04-07 12:10:25"}
{"backend_square_terminal_code_id":"9","square_device_code_id":"7M14JAR5FMGAR","code_value":"HCWMAB","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"EXPIRED","location_id":"5W9V4C95PWDVQ","pair_by":null,"paired_at":"1970-01-01 00:00:00","raw_json":"{\"id\":\"7M14JAR5FMGAR\",\"name\":\"Front Desk Terminal\",\"code\":\"HCWMAB\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"created_at\":\"2026-04-07T16:10:16.000Z\",\"status\":\"EXPIRED\",\"status_changed_at\":\"1970-01-01T00:00:00.000Z\"}","created_at":"2026-04-07 12:10:16","updated_at":"2026-04-07 12:22:56"}
[]
{"backend_square_terminal_code_id":"9","square_device_code_id":"7M14JAR5FMGAR","code_value":"HCWMAB","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"UNPAIRED","location_id":"5W9V4C95PWDVQ","pair_by":"2026-04-07 16:15:16","paired_at":"2026-04-07 16:10:16","raw_json":"{\"id\":\"7M14JAR5FMGAR\",\"name\":\"Front Desk Terminal\",\"code\":\"HCWMAB\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"pair_by\":\"2026-04-07T16:15:16.000Z\",\"created_at\":\"2026-04-07T16:10:16.000Z\",\"status\":\"UNPAIRED\",\"status_changed_at\":\"2026-04-07T16:10:16.000Z\"}","created_at":"2026-04-07 12:10:16","updated_at":"2026-04-07 12:10:22"}
{"backend_square_terminal_code_id":"9","square_device_code_id":"7M14JAR5FMGAR","code_value":"HCWMAB","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"UNPAIRED","location_id":"5W9V4C95PWDVQ","pair_by":"2026-04-07 16:15:16","paired_at":"2026-04-07 16:10:16","raw_json":"{\"id\":\"7M14JAR5FMGAR\",\"name\":\"Front Desk Terminal\",\"code\":\"HCWMAB\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"pair_by\":\"2026-04-07T16:15:16.000Z\",\"created_at\":\"2026-04-07T16:10:16.000Z\",\"status\":\"UNPAIRED\",\"status_changed_at\":\"2026-04-07T16:10:16.000Z\"}","created_at":"2026-04-07 12:10:16","updated_at":"2026-04-07 12:10:25"}
[]
{"backend_square_terminal_code_id":"9","square_device_code_id":"7M14JAR5FMGAR","code_value":"HCWMAB","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"UNPAIRED","location_id":"5W9V4C95PWDVQ","pair_by":"2026-04-07 16:15:16","paired_at":"2026-04-07 16:10:16","raw_json":"{\"id\":\"7M14JAR5FMGAR\",\"name\":\"Front Desk Terminal\",\"code\":\"HCWMAB\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"pair_by\":\"2026-04-07T16:15:16.000Z\",\"created_at\":\"2026-04-07T16:10:16.000Z\",\"status\":\"UNPAIRED\",\"status_changed_at\":\"2026-04-07T16:10:16.000Z\"}","created_at":"2026-04-07 12:10:16","updated_at":"2026-04-07 12:10:19"}
{"backend_square_terminal_code_id":"9","square_device_code_id":"7M14JAR5FMGAR","code_value":"HCWMAB","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"UNPAIRED","location_id":"5W9V4C95PWDVQ","pair_by":"2026-04-07 16:15:16","paired_at":"2026-04-07 16:10:16","raw_json":"{\"id\":\"7M14JAR5FMGAR\",\"name\":\"Front Desk Terminal\",\"code\":\"HCWMAB\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"pair_by\":\"2026-04-07T16:15:16.000Z\",\"created_at\":\"2026-04-07T16:10:16.000Z\",\"status\":\"UNPAIRED\",\"status_changed_at\":\"2026-04-07T16:10:16.000Z\"}","created_at":"2026-04-07 12:10:16","updated_at":"2026-04-07 12:10:22"}
[]
{"backend_square_terminal_code_id":"9","square_device_code_id":"7M14JAR5FMGAR","code_value":"HCWMAB","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"UNPAIRED","location_id":"5W9V4C95PWDVQ","pair_by":"2026-04-07 16:15:16","paired_at":"2026-04-07 16:10:16","raw_json":"{\"id\":\"7M14JAR5FMGAR\",\"name\":\"Front Desk Terminal\",\"code\":\"HCWMAB\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"pair_by\":\"2026-04-07T16:15:16.000Z\",\"created_at\":\"2026-04-07T16:10:16.419Z\",\"status\":\"UNPAIRED\",\"status_changed_at\":\"2026-04-07T16:10:16.000Z\"}","created_at":"2026-04-07 12:10:16","updated_at":"2026-04-07 12:10:16"}
{"backend_square_terminal_code_id":"9","square_device_code_id":"7M14JAR5FMGAR","code_value":"HCWMAB","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"UNPAIRED","location_id":"5W9V4C95PWDVQ","pair_by":"2026-04-07 16:15:16","paired_at":"2026-04-07 16:10:16","raw_json":"{\"id\":\"7M14JAR5FMGAR\",\"name\":\"Front Desk Terminal\",\"code\":\"HCWMAB\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"pair_by\":\"2026-04-07T16:15:16.000Z\",\"created_at\":\"2026-04-07T16:10:16.000Z\",\"status\":\"UNPAIRED\",\"status_changed_at\":\"2026-04-07T16:10:16.000Z\"}","created_at":"2026-04-07 12:10:16","updated_at":"2026-04-07 12:10:19"}
[]
[]
{"backend_square_terminal_code_id":"9","square_device_code_id":"7M14JAR5FMGAR","code_value":"HCWMAB","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"UNPAIRED","location_id":"5W9V4C95PWDVQ","pair_by":"2026-04-07 16:15:16","paired_at":"2026-04-07 16:10:16","raw_json":"{\"id\":\"7M14JAR5FMGAR\",\"name\":\"Front Desk Terminal\",\"code\":\"HCWMAB\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"pair_by\":\"2026-04-07T16:15:16.000Z\",\"created_at\":\"2026-04-07T16:10:16.419Z\",\"status\":\"UNPAIRED\",\"status_changed_at\":\"2026-04-07T16:10:16.000Z\"}","created_at":"2026-04-07 12:10:16","updated_at":"2026-04-07 12:10:16"}
[]
{"backend_square_terminal_code_id":"1","square_device_code_id":"5JVYM58J39RMV","code_value":"NJQDJK","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"EXPIRED","location_id":"5W9V4C95PWDVQ","pair_by":null,"paired_at":"1970-01-01 00:00:00","raw_json":"{\"id\":\"5JVYM58J39RMV\",\"name\":\"Front Desk Terminal\",\"code\":\"NJQDJK\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"created_at\":\"2026-04-07T16:00:25.000Z\",\"status\":\"EXPIRED\",\"status_changed_at\":\"1970-01-01T00:00:00.000Z\"}","created_at":"2026-04-07 12:00:24","updated_at":"2026-04-07 12:09:12"}
{"backend_square_terminal_code_id":"1","square_device_code_id":"5JVYM58J39RMV","code_value":"NJQDJK","device_name":"Front Desk Terminal","product_type":"TERMINAL_API","code_status":"EXPIRED","location_id":"5W9V4C95PWDVQ","pair_by":null,"paired_at":"1970-01-01 00:00:00","raw_json":"{\"id\":\"5JVYM58J39RMV\",\"name\":\"Front Desk Terminal\",\"code\":\"NJQDJK\",\"product_type\":\"TERMINAL_API\",\"location_id\":\"5W9V4C95PWDVQ\",\"created_at\":\"2026-04-07T16:00:25.000Z\",\"status\":\"EXPIRED\",\"status_changed_at\":\"1970-01-01T00:00:00.000Z\"}","created_at":"2026-04-07 12:00:24","updated_at":"2026-04-07 12:09:16"}