IBO Pro Ultra — Reseller Bundle v18

panel/ + apk/IBO_Pro_Ultra.apk (generic — placeholder URL baked in)

What's new in v18
-----------------
One belt-and-suspenders fix on top of v17 for a real buyer install
that broke Quick URL Encrypt.

0. api/quick_url_encrypt.php now reads crypto constants DIRECTLY
   from includes/apksec_constants.php, bypassing db_config()
   -----------------------------------------------------------------
   Previously the endpoint went through db_config() which reads panel.db
   config row and falls back to apksec_constants.php only if the DB value
   is empty. If the DB was readonly (perm mismatch), had a partial init,
   or had a legacy non-empty seed value that later became invalid, the
   endpoint could produce a bad literal (or throw
   "working_key_hex must be 16 raw bytes"). No DB state can now cause a
   crypto misconfiguration — the endpoint reads the file straight.
   Same applies to the verify path.

What's new in v17
-----------------
Same as v16 (see below), minus one install-day headache:

0. data/panel.db is no longer shipped in the zip
   -----------------------------------------------
   Previously the zip contained a 0-byte `panel/data/panel.db` placeholder.
   On many shared-hosting setups the extract left it owned by root (or
   whatever user did the extract) while Apache ran as www-data — so
   Apache couldn't overwrite the file, panel_db() failed with
   `SQLSTATE[HY000]: General error: 8 attempt to write a readonly database`,
   schema never initialized, addaplaylist never registered, and Quick URL
   Encrypt threw. Fix was previously "delete the file so PHP recreates it";
   now v17 just doesn't ship it. `panel/data/` is still shipped as an
   empty directory. PHP creates panel.db on first hit with the correct
   owner and permissions.

   (Same reasoning for `panel/uploads/slideshows/` — the slideshow upload
   code auto-mkdirs it, and the empty dir ships without any file inside.)

What's new in v16 (kept for reference)
--------------------------------------
This release is mostly quality-of-life on top of v15, plus one silent-fatal
schema bug that would break Quick URL Encrypt on every fresh install.

1. Schema seed no longer poisons Quick URL Encrypt on new installs
   -----------------------------------------------------------------
   includes/db.php CREATE TABLE config had a hardcoded default value for
   `target_server_key_hex` (`0370dd1afec23dabd2639452ca6a4099`). Because
   the column wasn't empty on fresh installs, `db_config()`'s fallback
   to `apksec_constants.php` never fired — the panel encrypted URLs
   using the seeded garbage instead of the reseller working key.

   Symptom: buyer runs Quick URL Encrypt, bakes the literal into the
   APK, installs, and gets `Bad URL <Unicode-garbage>/auth`.

   Fix: default cleared to `''` in the schema, the INSERT, and the
   ALTER TABLE. Fresh installs now hit the apksec_constants fallback
   as intended.

   If you already deployed v15 to a customer: fix in-place with
     UPDATE config SET target_server_key_hex = '' WHERE id = 1;

2. Quick URL Encrypt now forces a trailing slash before encrypting
   ---------------------------------------------------------------
   The IBO app's GlassVpnPicker builds fetch URLs as
   `_panelUrl + "vpns.php"` — no leading slash on the endpoint side.
   If the buyer pasted a URL without a trailing slash, the app
   fetched `.../slugvpns.php` and threw "Could not load VPN list".

   Fix: `api/quick_url_encrypt.php` appends a `/` before encryption
   if one isn't present. Buyers can't paste a URL that breaks half
   the app.

3. Verify a literal — new tool on the Quick URL Encrypt page
   --------------------------------------------------------
   Paste any `ct_hex|iv_hex` string from an APK's PanelUrl.smali line
   14; panel decrypts it locally with the reseller constants and
   reports the URL it will decode to on-device. Three outcomes:
     ✓ Decrypts cleanly → URL shown, safe to install
     ⚠ Decrypts but no trailing slash → yellow warning
     ✗ Decrypts to garbage → specific reason (customer format,
       wrong keys, non-hex characters, etc.)
   Answers "was this literal generated by THIS panel?" in one click.

4. VPN URL-fetch now handles Dropbox and Google Drive share links
   ---------------------------------------------------------------
   The Add VPN Config modal's URL field auto-rewrites two common
   share-URL formats so paste-and-go works:
     Dropbox   dropbox.com/s/…  or  /scl/…   → forces ?dl=1
     G. Drive  drive.google.com/file/d/ID/view → uc?export=download&id=ID
   Any other host is fetched as-is. If it returns HTML (login page,
   error), the panel's `remote` directive check rejects it with a
   human-readable toast so the buyer knows the URL was bad.

5. Framed View — Layout 0 (fullscreen 1 slot) + backdrop toggle
   ------------------------------------------------------------
   Framed View gains a new "Layout 0 — Fullscreen (1 slot)" option
   and a top-level checkbox "Use this as the home backdrop". When
   the toggle is on, the panel resolves `home_url1` in /auth to
   `api/webview/frameview.php` instead of the TMDB backdrop.
   End-to-end flow: upload slideshow images → Framed View →
   Layout 0 → drop Slideshow in the only slot → toggle on → save →
   next /auth on the box swaps the backdrop. Fully reversible.

6. Media Slideshow — new Image Fit dropdown
   -----------------------------------------
   "Fit" (default; whole image visible, letterbox if aspect differs)
   vs "Fill" (edge-to-edge, may crop). Portrait / square photos no
   longer zoom-crop on the TV. Setting persists in webview_settings
   under `slide_fit_mode`.

7. Slideshow upload no longer requires the fileinfo extension
   -----------------------------------------------------------
   `finfo_open()` on a host without the fileinfo extension threw a
   500. Now the panel falls back to `getimagesize()` (always
   available with GD, standard on cPanel). Still rejects renamed
   PHP/HTML because getimagesize returns false for non-images.

8. New Support Tools sidebar section
   ----------------------------------
   Two buyer-facing pages under Diagnostics:
     Health Check   pass/fail per requirement — PHP version, required +
                    optional extensions, ini limits, file permissions,
                    apksec+crypto variant, addaplaylist registration,
                    outbound HTTPS test. Buyer self-diagnoses without
                    needing the support token.
     Self Test      one-click probe of every panel endpoint the app
                    depends on (auth, has_playlist, vpns, webview
                    endpoints), plus outbound HTTPS. Reports HTTP
                    code + pass/fail + ms per endpoint.

9. Hidden debug.php — support-mode diagnostic dump
   ------------------------------------------------
   /debug.php returns 404 unless the support token is passed as
   ?key=<TOKEN>. Token lives in includes/debug_constants.php
   (rotate the string to invalidate). With the right token, the
   page shows in one screen:
     - Sticky action bar (Copy report, Download .txt, Open plain
       text, Export panel state .zip, Clear errors, Clear requests,
       Clear BOTH)
     - Overall HEALTHY / MINOR ISSUES / ATTENTION status pill
     - Auto-refresh checkbox (10 s)
     - PHP + Reseller sanity + Extensions + ini + config + File
       perms + panel.db counts + Recent request log + Recent error
       log — all with green/amber/red status per section, per-section
       Copy button, and helpful empty states
   Also supports ?format=txt for plain-text output the buyer can
   paste into chat with one click. Filename is dynamic
   (panel-state-<host>-<timestamp>.zip / .txt) — nothing hardcoded.

Everything from v15 still in place
----------------------------------
- Fully redacted panel — no apk_builder, no builder.php, no
  master.db refs, no proprietary paths
- RESELLER-VARIANT apksec_constants.php + crypto.php
- .htaccess with all rewrite rules
- vpn.php + api/vpns.php URL-fetch feature
- vpns.php empty-cred filter
- add.php returns public_id
- delete.php accepts playlist_id + row-PK fallback
- APK AndroidManifest — foregroundServiceType="specialUse" (Android 14)
- APK res/layout/userpass.xml — real fields with @id/username etc.
- APK signed with reseller keystore (cert SHA 9210b240…d283d7); installs
  upgrade in place on end-user devices

If you already deployed v15 to a customer's host
------------------------------------------------
Overwrite these files from the v16 zip:

    panel/includes/db.php                 (schema seed fix)
    panel/api/quick_url_encrypt.php       (trailing slash + verify)
    panel/api/vpns.php                    (Dropbox/Drive rewriting)
    panel/quick_url_encrypt.php           (verifier UI)
    panel/frameview_settings.php          (Layout 0 + backdrop toggle)
    panel/api/webview/frameview.php       (Layout 0 rendering)
    panel/api/webview/frames.php          (backdrop toggle save)
    panel/includes/client_common.php      (home_url1 resolution)
    panel/api/webview/slideshow.php       (fit mode)
    panel/api/webview/slideshows.php      (fileinfo fallback + fit mode API)
    panel/slideshow_settings.php          (fit mode UI)
    panel/health.php                      (new)
    panel/selftest.php                    (new)
    panel/debug.php                       (new)
    panel/includes/debug_constants.php    (new)
    panel/modules/support/module.php      (new sidebar section)

And on their live DB, one row to clear the stale schema default:
    sqlite3 data/panel.db \
      "UPDATE config SET target_server_key_hex = '' WHERE id = 1;"

Prior fixes still in place (rollup)
-----------------------------------
v15: proper reseller-variant crypto.php restored (was clobbered in v11–v14);
     .htaccess restored to the panel/ root
v14: proprietary APK builder infrastructure stripped from the zip
v13: restored reseller-variant apksec_constants.php that v12 clobbered
v12: api/vpns.php + vpn.php — URL fetch for external OVPN
v11: AndroidManifest — foregroundServiceType="specialUse" (Android 14 fix)
     + api/vpns.php filter for empty-creds profiles
     + /add.php returns public_id (contract match with /auth)
     + /delete.php accepts playlist_id + row-PK fallback
v10: languages editor — full 217-key manual editor, Save preserves all
v9:  TMDB dropdowns — added pl-PL metadata + PL region
v8:  VPN Add form (u/p optional, enabled checkbox honored)
     + 8-language seed (1736 translations)
v7:  delete_user_row atomic action (playlist + mac-if-orphan)
v6:  services_clients Edit / Lock buttons wired
v5:  generic placeholder-URL APK
v4:  full addaplaylist integration
v3:  quick_url_encrypt auto-detect keeps trailing slash
v2:  server-side trailing-slash enforcement
v1:  libapksec_reseller.so + apksec_constants.php as source of truth

Common questions
----------------
Q: A customer's QR code says "Device code expired or invalid"
A: The addaplaylist auto-register call may have failed during first-load
   (network timeout, blocked outbound). Re-run manually via SSH:

     cd /path/to/panel
     ADDAPLAYLIST_REGISTER_SECRET=<from includes/addaplaylist_constants.php> \
     PANEL_DOMAIN='<host>/<subpath>' \
     php cron/addaplaylist_register.php

Q: How do I access the debug page?
A: /debug.php?key=<the token in includes/debug_constants.php>
   Change the token by editing that file. The default token is baked in
   this bundle — rotate it if you're worried it's leaked.
