The web client calls auth/login to authenticate users, but this endpoint
was missing from the Hub API. Creates:
- api/hub/auth/login.php: password-based auth with token generation
- Hub_Users table: stores bcrypt password hashes and session tokens
- Auto-provisions on first login (creates credentials for existing agents)
- Adds route to PUBLIC_ROUTES in helpers.php
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
If git pull fails (corrupted .git), automatically remove the broken
.git dir and re-clone. This prevents the chicken-and-egg problem
where a broken repo state makes the deploy webhook permanently stuck.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New endpoints under /api/hub/channels/:
- create.php: Create channel with type (public/private/direct), auto-add creator as owner
- list.php: List channels with filters (type, agent membership, archived, pagination)
- get.php: Get channel by ID or Name, includes member list
- update.php: Update display name, purpose, archive status (admin/owner only)
- delete.php: Hard-delete channel (owner only), FK cascade removes members
- members.php: List channel members with agent info
- join.php: Join public channels (private requires invite)
- leave.php: Leave channel (owners blocked from leaving)
Database: Hub_Channels + Hub_ChannelMembers tables with FK cascade.
Task #59 (T51-Sub1)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Bot-to-bot endpoints don't have user tokens, so they need to
bypass auth middleware.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New endpoints at api/tasks/team/ for centralized bot task tracking:
- POST create.php — register a new task
- POST update.php — change status, add notes (auto-sets PausedOn/CompletedOn)
- GET active.php — list active/paused tasks, optional BotName filter
- GET list.php — full listing with filters (BotName, Status, Channel, AssignedBy, Since) and pagination
Includes schema.sql for the TeamTasks table.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. Switch str_contains() to exact match ($path === $route) in PUBLIC_ROUTES check
to prevent substring-based route bypass attacks.
2. Remove blanket /api/admin/ bypass that was letting all admin endpoints through
without authentication.
3. Add requireCronSecret() — cron/scheduled task endpoints now require a valid
X-Cron-Secret header matching the PAYFRIT_CRON_SECRET env var. Uses
hash_equals() for timing-safe comparison. Applied to:
- cron/expireStaleChats.php
- cron/expireTabs.php
- api/admin/scheduledTasks/runDue.php
Instead of recalculating the grand total from line items + rates (which
can drift by a penny due to floating point), use the actual PaymentFromCreditCard
or PaymentPaidInCash values from the Payments table. This ensures the receipt
always matches what the customer was actually charged.
Individual line prices were displayed rounded (via dollars()) but the
raw floating-point values were accumulated into the subtotal. This caused
totals like $0.99 instead of $1.00 when item prices had fractional cents.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rewrites the last two production-critical CFM endpoints for the biz.payfrit.com
Lucee removal project. Both endpoints follow the existing helpers.php patterns
with queryTimed/queryOne and are added to PUBLIC_ROUTES.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
submitCash.php, submit.php, and webhook.php were creating kitchen
tasks without TaskTypeID, which is NOT NULL with no default. This
caused cash order submission to fail with a SQL error.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
getDetail.php was computing total as subtotal + tax + tip, omitting the
Payfrit service fee. This caused the order detail view to show a lower
total than what the cart/checkout displayed at order time.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
submitCash.php had no task creation — cash orders were invisible to KDS.
submit.php also lacked it (tab orders never hit webhook.php).
Both now create "Prepare Order #X for Table" task at StatusID=1.
submit.php includes duplicate guard since webhook.php also creates tasks
for card payments.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
MySQL PDO returns DECIMAL/FLOAT columns as strings, causing json_encode
to emit them as JSON strings ("9.99") instead of numbers (9.99).
Android's Gson Map parsing fails the as? Number cast on strings,
defaulting to 0.0. Cast Price to (float) before building the response.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Only Call Staff, Chat With Staff, and Pay With Cash should appear
on the customer service bell. New column distinguishes service bell
items from internal task types.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Got reverted during earlier merge conflict resolution. Without cd,
node can't find modules. Without stderr redirect, log lines corrupt
the JSON output causing json_decode to fail.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- analyzeMenuUrl.php: Extract og:image and JSON-LD image during discovery, return as headerImageUrl
- downloadImages.php: Add User-Agent header, detect image format from content-type/magic bytes, update HeaderImageExtension in DB
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
order.online embeds displayPrice alongside images — now extracted and
returned so items missing prices from image-based menus get filled in.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
downloadItemImage() saved files to disk but never updated the DB column,
so items appeared to have no images despite files existing on disk.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Moved directory on both dev and biz servers
- Updated nginx configs on both servers
- Added appRoot() helper, uploadsRoot() uses it
- No more hardcoded /opt/payfrit-api paths in codebase
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Moved uploads from Lucee webroot to /opt/payfrit-api/uploads/
- Updated nginx on both dev and biz to alias /uploads/ to new path
- Replaced luceeWebroot() with uploadsRoot() helper
- Temp files now use /opt/payfrit-api/temp/
- No more /opt/lucee or /var/www/biz.payfrit.com references
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Added luceeWebroot() helper to avoid repeating the path. The previous
fix incorrectly used /var/www/biz.payfrit.com for production, but both
dev and biz use the same Lucee webroot.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Upload endpoints were saving files to PHP's DOCUMENT_ROOT instead of
the Lucee webroot where the Android app loads them from. Also fix
verifyLoginOTP and verifyOTP to accept both UUID/OTP and uuid/otp keys.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Detect contact/about/location/hours links on main page
- Fetch contact page and extract phone, address, hours
- Phone: regex for US phone formats + tel: links
- Address: US street address pattern (number + street type)
- Hours: day + time range patterns from plain text
- Overrides bad JSON-LD data with actual contact page info
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Prefer title tag for name over JSON-LD (sites often put address in LD name)
- Parse full address string into components (addressLine1, city, state, zip)
- Handle newlines in addresses (Squarespace puts newlines in JSON-LD)
- Convert 24h hours to 12h format
- Strip country suffix from addresses
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Claude prompt now asks for menuSchedule (days/times this menu is served)
- Separates menu schedule from overall business hours
- Returns menuSchedule in response for frontend to use
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Parse JSON-LD structured data (Restaurant, FoodEstablishment, etc.)
- Extract phone from tel: links, address from og: meta tags
- Return businessInfo in discovery response so sub-pages don't need it
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Discovery mode: quick Playwright crawl returns detected menu sub-pages
- Extract_page mode: processes single menu page through Claude individually
- More aggressive HTML stripping: removes SVG, nav, footer, form, attributes
- Increased truncation limit from 100KB to 200KB for generic fallback path
- Enables interactive wizard flow: discover → confirm → extract each page
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude returns address as one string with country suffix.
Now strips "United States/USA", extracts ZIP, state, splits
address line and city server-side before sending to wizard.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Updated Claude prompt to detect separate menus vs categories
- Added platformImageMap and subPagesVisited parsing from Playwright
- Bumped Playwright wait from 5s to 10s for sub-page crawling
- saveWizard.php creates separate Menus rows and assigns categories/items
to the correct menu based on each item's "menu" field
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add sendSMS() to helpers.php using Twilio REST API with cURL,
credentials loaded from config/twilio.json. Wire into sendOTP,
loginOTP, and sendLoginOTP endpoints, replacing TODO stubs.
SMS is auto-skipped on dev environments.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>