Background tasks and troubleshooting

Reference for the files delivered in Main_File. Documentation updated 22 September 2026. Examples use fictional IDs and balances; replace them with values from your installation.

Scheduled and background work: what actually runs

The supplied PHP application has no cron entry point, job scheduler or durable queue worker. Do not install a fictitious VIP-expiry cron. PHP handles the work below during requests; Node.js is the continuously running Socket.IO/Redis relay. There is no automatic offline replay of missed socket messages.

Work Trigger and implementation Setup and manual recovery
VIP expiration AppChatController.activeVip selects active records with expires_at > UTC_TIMESTAMP(). User cache TTL is capped at the VIP expiry. Shop activation rejects expired ownership. No cron needed. Keep server/DB time correct. Refresh auth/tokenLogin or user/index after expiry. Historical user_vips rows are retained; expiry does not require deleting them. A stale open app may need a profile refresh.
User levels Gift sending adds gifter/anchor points; verified purchases add charge points. addUserLevelPoints updates totals and user_levels. levelsForUser also derives levels from totals when a profile is rebuilt. No scheduled batch. After editing level thresholds, refresh affected users via auth/tokenLogin or wait for their profile cache to expire (up to one hour). For damaged totals, reconcile against transaction history on a backup; there is no bundled bulk repair command. Never replay gifts/payments to recalculate a level.
Rankings live/ranking aggregates gift_transactions on each request. Daily UTC, Sunday–Saturday week, or calendar month. Rocket ranking aggregates the current UTC day's rocket_records. New periods are query windows, not destructive resets. No midnight cron needed. Confirm transaction timestamps, selected type/time/room and indexes. Refresh the ranking. Preserve transaction records.
Rocket rewards During sendGift, updateRocketProgress detects level transition and distributes configured rewards, then emits rocket/rocketProgress. No independent reward daemon. Check rocket_levels, rocket_rewards and wallet records before correcting a missing reward. There is no automatic replay job.
Inventory expiry Inventory queries and equipment selection filter expires_at at request time; cached profiles can remain until refresh/TTL. No cleanup task. Refresh inventory and profile. Retain ownership history.
Room coin boxes Creation stores the timer. Claim checks the clock and remaining slots/coins during livebox/use. Room reads filter the visible boxes. No timer worker. Refresh room state; do not infer eligibility solely from a client countdown. No separate automatic refund job is included.
Presence and room closure Socket connect/join/leave/disconnect updates Redis sets. Admin room closure clears room state and publishes stopRoom. Run Redis and Node continuously. Abrupt process death can leave stale presence. Restart Node, reconnect clients and re-read room data. Diagnose specific stale room/user keys; do not FLUSHALL a shared Redis database.
FCM and purchase verification PHP sends notification requests and verifies receipts synchronously in the triggering request. No retry queue. Inspect PHP logs and payment_logs. Do not assume PM2 will retry failed notifications or financial requests.
Admin sessions 12-hour expiration checked against UTC_TIMESTAMP() when a session is read. No expiry cron; sign in again. Optional retention/cleanup is an operator policy and not supplied here.
Room moderation expiry userAction can store expires_at, but roomRole in this package reads the role without enforcing that timestamp. Do not promise timed automatic unbans. An authorized operator must manually restore the intended member role, then verify room access. Adding timed enforcement requires a separate code change.

Keep the real-time relay alive

Use the production process-management guide for PM2 startup, explicit Node .env loading, logs, restarts and reboot recovery. PHP and Node must share Redis connection settings and API_KEY. Node does not read .env automatically, and SOCKET_BIND_IP does not bind its listener; follow the environment and firewall steps.

Troubleshooting with errors, ordered diagnosis and proof

The literal app errors below are taken from the supplied handlers; infrastructure messages are representative examples whose paths and wording vary by host. They are not claimed to be logs captured from your production server. Keep tokens, keys and personal data out of support attachments.

1. API responds with HTML, 404 or a JSON parsing error

FormatException: Unexpected character (at character 1)
<!DOCTYPE html>
{"code":404,"err":["Controller ... not found."],"data":null}
  1. Inspect the response body and exact URL before debugging Flutter models.
  2. Confirm APPCHAT_API_BASE_URL is the public base directory; the client appends api/. Avoid /api/api/. Confirm APP_BASE_PATH matches your deployment subdirectory.
  3. Verify Apache rewrite/Nginx front controller points to public/index.php; static HTML and admin URLs are not mobile API URLs.
  4. POST {} to api/helper/config with Content-Type: application/json and API-KEY.

Fixed when: the response is JSON with code=200 and data.agora/socket_ip/socket_port, then the rebuilt client reads the same values.

2. Login/token rejected

{"code":401,"err":["invalid_token"],"msg":["invalid_token"],"data":null}
loginFailed: {"message":"Invalid API key"}
  1. Distinguish the API-KEY from the mobile Auth-Token and the separate Admin-Token.
  2. Compare build-time API key with PHP API_KEY and Node API_KEY. Rebuild the app after changing dart-define.
  3. Sign in again; a new device login invalidates old tokens. Check device_id matches the login device.
  4. If PHP works but Node says Invalid token, check both services' Redis configuration, then call auth/tokenLogin to repopulate the Redis mapping before socket conn.

Fixed when: auth/tokenLogin returns code=200, socket receives appCon=1, and no loginFailed/forceLogout follows unexpectedly.

3. Database connection/import failure

SQLSTATE[HY000] [1045] Access denied for user ...
SQLSTATE[42S02]: Base table or view not found: 1146 ... user_vips ...
  1. Read PHP/web-server logs. An invalid_token response can mask a database failure because token lookup catches database exceptions.
  2. Verify DB_HOST, DB_PORT, DB_DATABASE, DB_USERNAME and DB_PASSWORD in PHP .env (mapped internally to LEGACY_SQL_* keys) and that the service is running.
  3. Use the same account to connect to the intended database; check grants.
  4. For missing tables, compare against appchat_clean_install.sql. Import into a new empty database; never reimport the clean installer over live data.

Fixed when: helper/config and authenticated user/index work and expected tables exist in the same database PHP uses.

4. Redis/Socket.IO offline or 502

Redis Client Error Error: connect ECONNREFUSED 127.0.0.1:6379
Redis subscriber Error ...
WebSocket connection ... failed: Error during WebSocket handshake: Unexpected response code: 502
  1. Run pm2 status and inspect the relay logs. Resolve startup errors before changing Flutter.
  2. Check Redis PING on the configured host and credentials; do not print passwords in tickets.
  3. Check Node PORT against the reverse-proxy upstream and allow only the required private connectivity.
  4. Verify HTTPS certificate, socket hostname, /socket.io/ proxy path and Upgrade/Connection headers.
  5. Reconnect using conn with a valid token, then enter the room through PHP.

Fixed when: logs show redis connect Success, appCon=1 arrives, and two devices exchange roomMessage. PHP success alone does not confirm real-time delivery.

5. Room join crashes or produces no seat/chat updates

TypeError: Cannot set properties of undefined (setting 'users_count')
{"code":200,"data":{"state":"bad_password","err":"bad_password"}}
  1. For the TypeError, inspect the joinRoom payload: this server dereferences data.user. A direct joinRoom must include user:{} or the authorized user resource.
  2. Avoid conn.rid in this version because its internal join call omits user; authenticate first, then POST live/enterRoom.
  3. Check data.state, room password and membership restrictions before treating code=200 as entry success.
  4. Check the PHP→Redis update channel and Node subscription, then re-enter through PHP after reconnect.

Fixed when: enterRoom reaches room listeners and subsequent seat actions produce a roomSeats array. Do not send seatTake or sendGift as socket events.

6. Agora room opens but audio fails

room.user_sw_token: ""
# Representative SDK symptom: token invalid / token expired / microphone permission denied
  1. Check helper/config data.agora, then room.agora_channel, agora_uid and user_sw_token returned by live/enterRoom.
  2. An empty token means the backend Agora app ID/certificate is absent or still a CHANGE_ME placeholder.
  3. Use credentials from the same Agora project and the exact channel/UID used to mint the token; confirm clocks and TTL.
  4. Grant microphone permission on both devices; on iOS add NSMicrophoneUsageDescription before building.

Fixed when: two physical devices join the same room, take seats and hear each other; repeat after leaving and re-entering.

7. Recharge purchased but wallet unchanged

no_charge_rule
repeated_charge
invalid_payment_order
  1. Read payment/pay's JSON error and payment_logs status/error_text for the purchase. Do not grant coins merely because the client reported a purchase.
  2. For no_charge_rule, match product_id to the active recharge_packages Android/iOS product ID.
  3. Check provider, receipt field, package/bundle ID, service account/API permissions and platform tester account.
  4. For repeated_charge, look for the already credited order, refresh cash/index and compare wallet_transactions; duplicate verification is not a second credit.

Fixed when: a new sandbox purchase has one successful payment record, exactly one expected wallet credit, and the app shows the new balance.

8. Gift/store/VIP rejected

not_enough_coins
item_not_found
vip_not_owned
vip_expired
  1. Refresh cash/index and the relevant gift/store/VIP catalogue.
  2. Use the catalog ID for purchase and the owned inventory ID for equip. Check state=9 and correct recipient.
  3. For gifts calculate unit cost × count × recipient count; backpack sends require enough owned quantity.
  4. For VIP compare user_vips.expires_at with UTC_TIMESTAMP(); use an owned unexpired package when activating.

Fixed when: the single intended transaction succeeds, balance changes once, and inventory/VIP refresh agrees with the result.

9. Upload fails or image is broken

Invalid upload token
Unsupported upload type.
Bunny storage is not configured.
Upload too large
  1. Obtain a fresh upload plan and keep the entire signed URL; use PUT with binary bytes.
  2. Check the selected driver, allowed extension, PHP/web-server upload limits and the application's 25 MiB limit (26,214,400 bytes).
  3. For local storage check writable destination directories; for Bunny check storage region, zone, key and CDN hostname.
  4. Open the returned display URL separately. An upload endpoint and its public display URL serve different purposes.

Fixed when: PUT returns HTTP 200 and OK (server driver), GET displays the saved file, and a second device loads the image.

10. Moderator cannot see a section or can see too much

Permission denied: reports.manage
Invalid request token
  1. Confirm the active admin account, role state=9 and assigned roles.
  2. Add both the view permission and intended action permission. Permissions across roles are combined; remove unintended broad roles.
  3. Sign out and back in. For CSRF errors reload the form in that session and submit the current _csrf value.
  4. Test both the sidebar and the direct restricted page URL.

Fixed when: allowed operations work, restricted page/action requests fail, and the audit log records the permitted change.

11. VIP/level/ranking seems stale

# Example symptom, not a server exception:
VIP badge remains visible after expires_at; profile/ranking has old points.
  1. Compare UTC database time and expiry/transaction timestamps.
  2. Refresh auth/tokenLogin for the affected user and fetch ranking again with the intended type/time/rid.
  3. Confirm the underlying gift/payment transaction committed and the user point totals changed.
  4. If totals differ, preserve records and reconcile on a backup; no cron or replay worker is included.

Fixed when: expired VIP becomes null, level matches the configured threshold and ranking reflects transactions in its UTC period.

12. Flutter/iOS configuration or signing failure

[core/no-app] No Firebase App '[DEFAULT]' has been created
No profiles for 'com.example.appchat' were found
Signing for "Runner" requires a development team
  1. Run flutter doctor -v on the Mac; install the required Xcode components.
  2. Register your actual bundle ID in Firebase and Apple, add the iOS Firebase plist to the Runner target, and regenerate firebase_options.dart.
  3. Open Runner.xcworkspace, select the Runner target and correct team, then enable automatic signing or install matching certificates/profiles.
  4. Build a development version on a physical device before archiving a release.

Fixed when: the device build launches and signs in, release archive validates, and App Store Connect processes the uploaded build.