================================================================================
  TIVIMATE 5.3.3 — DISCORD + WEATHER + SPORTS ADDON
  FULL INSTALL & SETUP GUIDE (public release)
================================================================================

This guide explains how to set up the whole addon from scratch:
  - The Discord bridge bot (Python) + where to get every setting
  - The panel changes (what we changed from the original panel)
  - How to expose the bot to the internet (cloudflared tunnel)
  - The APK edits (how Discord, Weather and Sports work)
  - Full install steps — where to put files and what to add

All real domain names, server URLs, folder paths and credentials have been
replaced with PLACEHOLDERS. Replace every YOUR_... / PASTE_... value with
your own.

================================================================================
  ⚡ EASY SETUP — THE 5-MINUTE VERSION
================================================================================
  If you just want it working fast, do these 5 things. Each is explained in
  full detail further down.

  1. SET UP THE BOT (Part 1)
     - Create a Discord bot at https://discord.com/developers/applications
     - Copy its token, client ID, client secret, and your room + server IDs
     - Paste them into  discord-bot/config.json  (replace the PASTE_... values)
     - On your server run:  bash install-and-run.sh

  2. GET A PUBLIC URL FOR THE BOT (Part 3)
     - On your server run:  bash install-cloudflared.sh
     - Copy the https://random-words.trycloudflare.com URL it prints
     - Put that URL in THREE places:
         a) discord-bot/config.json  ->  "public_base"
         b) panel config.ini         ->  [discord] bot_url
         c) Discord portal -> OAuth2 -> Redirects -> add  https://.../callback

  3. UPLOAD THE PANEL (Part 2 + 5.1)
     - Upload the whole panel folder to your web host
     - Edit config.ini: set your panel name + the bot_url from step 2
     - Log in (admin/admin) and CHANGE THE PASSWORD

  4. CHANGE THE PANEL URL IN THE APK (Part 4.5 — the ONLY APK edit)
     - Decompile the APK, open  AppConfig.smali, and change the panel URL
     -1. `smali_classes21/com/PanelURL.smali` → https://YOUR-PANEL-DOMAIN/YOUR-PANEL-FOLDER/ ✅
     -2. `smali_classes22/ca/krankhaus/tivi/config/AppConfig.smali` → https://YOUR-PANEL-DOMAIN/YOUR-PANEL-FOLDER/ ✅
     -3. `smali_classes22/ca/krankhaus/tivi/panel/PanelAutoImport.smali` → https://YOUR-PANEL-DOMAIN/YOUR-PANEL-FOLDER/ ✅
       (see Part 4.5 for the exact file + line)
     - Rebuild with the dex-swap method, zipalign, sign
     - Install the APK on the device

  5. USE IT
     - Open the app -> Discord / Weather / Sports buttons appear
     - Discord: "Log in with Discord" -> Authorize -> chat room
     - Weather: clock + forecast appear automatically
     - Sports: categories | clock | listings

  THAT'S IT. The rest of this guide is the full detail for each step.

================================================================================
  WHAT THIS ADDON DOES
================================================================================
  - DISCORD: a button in the app opens a chat room. Users log in with their
    real Discord account (OAuth2 "Log in with Discord"). Only members of
    YOUR Discord server can get in (Discord checks membership automatically).
  - WEATHER: a weather page with a live clock, 3-day forecast, auto-detects
    the viewer's location. No API key needed.
  - SPORTS: a sports guide page with categories | clock | listings, D-pad
    navigation and auto-scroll. Two modes are included (see below).

  There are 4 parts:
    1. BOT      — Python bot (discord.py + aiohttp + faster-whisper)
    2. TUNNEL   — cloudflared, gives the bot a public HTTPS URL
    3. PANEL    — PHP panel (the web host) with the new pages
    4. APK      — the TiviMate app with the Discord/Weather/Sports buttons

================================================================================
  PART 1 — THE DISCORD BOT (where to get every setting)
================================================================================
  The bot runs on a server (a Raspberry Pi, a VPS, or any always-on Linux
  box). It connects to Discord, watches ONE room, and exposes an HTTP API
  the TV panel calls.

  FILES (in the discord-bot/ folder):
    DiscordBot.py        <- the bot itself
    config.json          <- ALL your settings go here
    requirements.txt     <- Python dependencies
    install-and-run.sh   <- one-shot install + run script
    install-cloudflared.sh <- starts the public tunnel
    discord-bot.service  <- optional systemd auto-start (Linux)

  ------------------------------------------------------------------
  1.1  CREATE THE BOT (Discord Developer Portal)
  ------------------------------------------------------------------
  Go to:  https://discord.com/developers/applications
  Click "New Application", name it (e.g. "TiviMate Chat").

  a) BOT TOKEN  ->  config.json  "bot_token"
     Left menu -> Bot -> Reset Token -> Copy.
     This is the bot's login token. KEEP IT SECRET.

  b) PRIVILEGED INTENT
     Left menu -> Bot -> scroll to "Privileged Gateway Intents".
     Enable  MESSAGE CONTENT INTENT  (the bot needs it to read chat).

  c) OAUTH CLIENT ID  ->  config.json  "client_id"
     Left menu -> OAuth2 -> General -> Copy "Client ID".

  d) OAUTH CLIENT SECRET  ->  config.json  "client_secret"
     Left menu -> OAuth2 -> General -> "Reset Secret" -> Copy.
     KEEP IT SECRET.

  e) REDIRECT URI
     Left menu -> OAuth2 -> General -> "Redirects" -> Add:
       https://YOUR-PUBLIC-BOT-URL/callback
     (YOUR-PUBLIC-BOT-URL is the cloudflared URL from Part 3.)

  f) INVITE THE BOT TO YOUR SERVER
     Left menu -> OAuth2 -> URL Generator.
     Scopes:  bot  +  applications.commands
     Bot permissions:  Send Messages, Read Message History, View Channels
     Open the generated URL -> pick your server -> Authorize.

  ------------------------------------------------------------------
  1.2  GET YOUR ROOM + SERVER IDs
  ------------------------------------------------------------------
  In Discord:  Settings -> Advanced -> Developer Mode  ON.
    - Right-click your ROOM (channel) -> Copy Channel ID
        -> config.json  "room_id"
    - Right-click your SERVER name -> Copy Server ID
        -> config.json  "guild_id"

  ------------------------------------------------------------------
  1.3  FILL IN config.json
  ------------------------------------------------------------------
  {
    "bot_token":     "PASTE_YOUR_BOT_TOKEN_HERE",
    "client_id":     "PASTE_YOUR_OAUTH_CLIENT_ID_HERE",
    "client_secret": "PASTE_YOUR_OAUTH_CLIENT_SECRET_HERE",
    "room_id":       "PASTE_YOUR_DISCORD_CHANNEL_ID_HERE",
    "guild_id":      "PASTE_YOUR_DISCORD_SERVER_ID_HERE",
    "public_base":   "https://YOUR-PUBLIC-BOT-URL.trycloudflare.com",
    "panel_base":    "https://YOUR-PANEL-DOMAIN/YOUR-PANEL-FOLDER",
    "http_port":     8090
  }

  - public_base = the cloudflared URL (Part 3). The OAuth2 redirect must be
    reachable by Discord, so this MUST be a public HTTPS URL.
  - panel_base  = the public URL of your panel folder (Part 2). The OAuth2
    redirect points back to the panel's discord.php page.

  ------------------------------------------------------------------
  1.4  INSTALL + RUN THE BOT
  ------------------------------------------------------------------
  On the server, in the discord-bot/ folder:

      bash install-and-run.sh

  This creates a Python venv, installs discord.py + aiohttp + faster-whisper,
  and starts the bot in the background on port 8090. It prints a health-check
  URL:  http://127.0.0.1:8090/health

  To stop it later:   kill $(cat bot.pid)
  To restart it:      bash install-and-run.sh

  OPTIONAL — auto-start on boot (systemd):
  Edit discord-bot.service and set:
      User=YOUR_LINUX_USERNAME
      WorkingDirectory=/path/to/your/discord-bot
      ExecStart=/path/to/your/discord-bot/venv/bin/python DiscordBot.py
  Then:
      sudo cp discord-bot.service /etc/systemd/system/
      sudo systemctl daemon-reload
      sudo systemctl enable --now discord-bot
      sudo systemctl status discord-bot

  ------------------------------------------------------------------
  1.5  VOICE INPUT (optional)
  ------------------------------------------------------------------
  The bot has a /transcribe endpoint. The APK records audio from the device
  mic and POSTs it here; faster-whisper (runs locally on the server, no
  cloud, no API key) transcribes it and the text is auto-posted to the room.
  First voice use downloads the ~75MB "tiny" model (needs internet).

================================================================================
  PART 2 — THE PANEL (what we changed from the original)
================================================================================
  The panel is a PHP + SQLite admin panel for the TiviMate app. We took the
  original panel and ADDED the Discord, Weather and Sports pages, plus a few
  small changes. Here is exactly what changed.

  ------------------------------------------------------------------
  2.1  NEW FILES WE ADDED (at the panel root)
  ------------------------------------------------------------------
    discord.php   <- the Discord chat page (the app's Discord button opens it)
    weather.php   <- the Weather page (live clock + 3-day forecast)
    sport.php     <- the Sports page (categories | clock | listings)
    sports.php    <- Sports admin page (pick which sports, colours, timezone)
    sports-fixtures.php <- Sports admin page (ESPN fixtures toggle)

  ------------------------------------------------------------------
  2.2  NEW FILES WE ADDED (in the api/ folder)
  ------------------------------------------------------------------
    api/sports.php          <- config endpoint the app calls for the Sports menu
    api/sports-fixtures.php <- ESPN fixtures proxy (UK-first league list)
    api/webview.php         <- generic in-app WebView config endpoint
    api/dns.php             <- lightweight plain-JSON portal list for the
                               app's native bridge

  ------------------------------------------------------------------
  2.3  NEW FILES WE ADDED (in the includes/ folder)
  ------------------------------------------------------------------
    includes/table.php  <- MERGED. Added new DB tables/columns (see 2.5)

  ------------------------------------------------------------------
  2.4  WHAT WE EDITED IN THE ORIGINAL PANEL
  ------------------------------------------------------------------
  a) main.php  — SIMPLIFIED THE "ADD PLAYLIST" FORM.
     REMOVED:  the Type dropdown (Xtream/Stalker/M3U), the Username,
               Password and MAC Address fields, and the output field.
     KEPT:     only Name + Server URL.
     WHY:      users log in with their OWN Xtream Codes credentials when
               they open a playlist. The panel just stores the base URL.

  b) api.php  — handleGetDevices() SIMPLIFIED.
     REMOVED:  the per-type URL building (embedding username/password into
               the URL for xtream, appending / for stalker, etc.).
     KEPT:     passes the base server URL as-is, hardcodes type => 'xtream'.
     WHY:      matches the simplified form — the app prompts for credentials.

  c) .htaccess  — STRIPPED TO MINIMAL (CSP ONLY).
     REMOVED:  the HTTPS force-redirect, the clean-URL rewrites, and the
               security blocks (deny .db / config.ini / includes/).
     KEPT:     only the CSP header (needed for the sports Live Preview
               iframe) + X-Frame-Options SAMEORIGIN.
     WHY:      the full .htaccess caused a 500 on the whole panel on an
               HTTP-only host. KEEP IT MINIMAL — do NOT re-add rewrites or
               the HTTPS redirect.

  d) config.ini  — ADDED a [discord] section.
     ADDED:    bot_url = "https://YOUR-PUBLIC-BOT-URL.trycloudflare.com"
     This is the public URL of the bot (Part 3). The discord.php page reads
     it so the TV's browser knows where to talk to the bot.

  ------------------------------------------------------------------
  2.5  DATABASE CHANGES (includes/table.php)
  ------------------------------------------------------------------
  The original panel already had: user, dns, note, welcome, devices.
  We ADDED these tables (auto-created on first run):
    vpn_profiles          - VPN server profiles
    sports_config         - Sports menu config (enabled/title/url)
    webview_config        - generic WebView config
    sports_fixtures_config- sports widget settings (token, sports, colours,
                            timezone, time format)
    sports_fixtures_cache - cache for the ESPN proxy
    leagues               - sports leagues
    sport_events          - sports fixtures/events
    request_log           - API request logging (analytics)
    ip_geo_cache          - per-IP geolocation cache
    webauthn_credentials  - optional passkey 2FA

  If your panel's table.php already has some of these, just make sure the
  columns exist. The sports_fixtures_config table needs these columns:
    widget_token, widget_sports, widget_bg, widget_bgs,
    widget_timezone, widget_time12

  ------------------------------------------------------------------
  2.6  HOW MUCH CODE WE ADDED
  ------------------------------------------------------------------
  The addon adds roughly 50% more code to the panel. Of the panel's PHP
  code, about half is the new Discord / Weather / Sports features, and
  about half is the original panel (with the small edits in 2.4).

  ------------------------------------------------------------------
  2.7  SECURITY — HOW THE CODE IS HARDENED
  ------------------------------------------------------------------
  The panel is secured against common attacks. These protections are built
  into the original KRANKHAUS panel and kept in place:

  LOGIN & PASSWORDS
    - Passwords are stored as bcrypt hashes (configurable cost, default 12),
      never in plaintext.
    - Login is locked for 15 minutes after 5 failed attempts (brute-force
      protection).
    - Session timeout (default 15 minutes) — idle sessions expire.

  SESSIONS & CSRF
    - Session-based login with a single admin account.
    - CSRF tokens on every form (generated with random_bytes, verified with
      hash_equals) — blocks cross-site request forgery.

  INPUT HANDLING
    - All user input is sanitized.
    - Database queries use parameterized statements (SQLite wrapper) —
      blocks SQL injection.

  THE APP / API
    - The bot token and OAuth client secret live ONLY on the server
      (config.json) — never in the APK or the panel.
    - The TV only holds a short-lived session token issued after a
      successful Discord login.
    - Access to the Discord room is gated by Discord's own membership
      check — non-members get a 403 and can't get in.
    - The panel proxies external APIs (weather, sports) server-side, so the
      app never talks to third parties directly.

================================================================================
  PART 3 — EXPOSING THE BOT (cloudflared tunnel)
================================================================================
  The bot runs on your server behind your router, so it has no public URL.
  Discord's OAuth2 redirect and the TV's browser both need to reach it.
  The easiest way is a free cloudflared "quick tunnel" — no account needed.

  On the server, in the discord-bot/ folder:

      bash install-cloudflared.sh

  This installs cloudflared (if missing), checks the bot is up on port 8090,
  and starts a tunnel. It prints a public HTTPS URL like:

      https://random-words.trycloudflare.com

  THAT URL is your  YOUR-PUBLIC-BOT-URL.  You must set it in THREE places:
    1. bot config.json        -> "public_base"
    2. panel config.ini       -> [discord] bot_url
    3. Discord Developer Portal -> OAuth2 Redirects -> https://.../callback

  IMPORTANT: the free trycloudflare.com URL is RANDOM and CHANGES every time
  the tunnel restarts. After each restart you must update all three places
  above. (For a permanent URL, use a named cloudflared tunnel or a reverse
  proxy like nginx/Caddy on a VPS — same idea, stable address.)

  The tunnel runs in the foreground. Leave it running (or run it under
  systemd/tmux so it survives logout).

================================================================================
  PART 4 — THE APK (how Discord, Weather and Sports work)
================================================================================
  The APK is the TiviMate app with three extra menu buttons. It has a NATIVE
  INTEGRITY CHECK (libcockpitdotlol.so -> isTampered) that crashes the app
  if protected dex files change. So we use the "dex-swap" method: ONLY
  classes22.dex is modified; classes.dex and classes2-21 stay byte-identical.

  ------------------------------------------------------------------
  4.1  THE MENU BUTTONS
  ------------------------------------------------------------------
  The app fetches its menu config from the panel at launch. The panel's
  api.php / api/sports.php / api/webview.php return the URLs for the
  Discord, Weather and Sports pages. If the panel is down, no buttons show.

  ------------------------------------------------------------------
  4.2  HOW DISCORD WORKS
  ------------------------------------------------------------------
  - The app's Discord button opens discord.php in a WebView.
  - discord.php talks to the bot's HTTP API, ALL from the browser (JS):
        GET  /login      -> OAuth2 "Log in with Discord" URL
        GET  /callback   -> OAuth2 exchange (Discord redirects back here)
        GET  /me         -> who is logged in
        GET  /messages   -> recent messages in the room
        POST /send       -> post a message
        GET  /logout     -> revoke the session
  - The OAuth exchange is done CLIENT-SIDE in the browser, NOT server-side.
    Reason: the panel host can't reach the bot, but the TV's browser can
    (through the tunnel). The browser stores the session token in
    localStorage.
  - Only members of YOUR server get in. The bot checks Discord membership
    (scope "guilds.members.read") and returns 403 if the user isn't a member.
  - The bot token and OAuth secret live ONLY on the server (config.json) —
    never in the APK. The TV only holds a short-lived session token.

  VOICE: the on-screen 🎤 key in the keyboard records from the device's
  built-in mic, POSTs the audio to the bot's /transcribe endpoint, and the
  returned text is auto-posted. This bypasses the system recognizer (which
  on Fire TV routes to Alexa and talks back).

  ------------------------------------------------------------------
  4.3  HOW WEATHER WORKS
  ------------------------------------------------------------------
  - The app's Weather menu is a WebView pointed at weather.php.
  - weather.php shows a live clock + 3-day forecast (open-meteo API).
  - It auto-detects the viewer's location: it tries GPS first
    (navigator.geolocation), and falls back to IP (ip-api.com) if GPS is
    denied or unavailable.
  - IMPORTANT: the WebView JS must NOT call external APIs directly (CORS is
    blocked). So weather.php PROXIES both calls server-side (same-origin):
        weather.php?action=loc       -> returns the viewer's lat/lon
        weather.php?action=forecast  -> returns the 3-day forecast
  - No API key required. Fully automatic.

  ------------------------------------------------------------------
  4.4  HOW SPORTS WORKS (two modes)
  ------------------------------------------------------------------
  MODE A — sport-tv-guide widget (sport.php):
    The app's Sports menu loads sport.php, which fetches the
    sport-tv-guide.live widget server-side and renders a clean layout:
        [ category icons row ] [ clock ] [ sports listings ]
    - data-src -> src rewrite so icons show in the app WebView.
    - LEFT/RIGHT moves across category icons both ways (wraps).
    - DOWN/UP left to NATIVE WebView focus (listings scroll + auto-scroll).
    - Category selection uses location.replace() so the remote BACK goes
      straight to the main menu in one press.
    - Auto-scroll only runs in the app (window.self !== window.top disables
      it in the panel Live Preview iframe).
    - sports.php (admin) lets you pick which sports, colours, timezone and
      time format, with a Live Preview.

    SPORTS CATEGORIES AVAILABLE (pick any in the panel's Sports admin page):
      Football, Ice Hockey, Basketball, Handball, Motorsport, Tennis,
      Volleyball, Golf, Winter Sports, Combat Sports, Snooker, Cycling,
      Rugby Union, Extreme Sports, Athletics, Cricket, Baseball,
      Horse Racing, Darts, Am. Football, Rodeo, Bowling, Sailing / Boating,
      Water Sports, Aussie Rules, Rugby League, Table Tennis, Squash,
      Field Hockey, Gymnastics, Netball, Equestrian, GAA, Badminton,
      Lacrosse, Boxing, MMA, WWE, Futsal, Fencing, Beach Soccer, Kabaddi,
      Shooting, Floorball, Triathlon, Weightlifting, Esport, Alpine Ski,
      Ski Jumping, Biathlon

  MODE B — ESPN fixtures (webview.php?fetch=sport):
    The app's Sports menu can instead load webview.php?fetch=sport, an
    IBO-style 3-pane guide (Sports | Leagues | Events) that auto-imports
    today's fixtures from ESPN's mobile endpoint (UK first). Fully
    automatic — re-imports if stale (>6h) or empty.

    SPORTS COVERED (ESPN, UK first):
      Football (English Premier League, Championship, League One/Two, FA Cup,
      EFL Cup, Scottish Premiership, League of Ireland, German Bundesliga,
      Italian Serie A/B, Spanish La Liga, French Ligue 1, Dutch Eredivisie,
      Portuguese Primeira Liga, Belgian Pro League, Turkish Super Lig,
      Greek Super League, Danish Superliga, Austrian Bundesliga, Swiss Super
      League, Norwegian Eliteserien, Swedish Allsvenskan, Russian Premier
      League, UEFA Champions/Europa/Conference/Nations, Copa Libertadores,
      Copa Sudamericana, CONCACAF League, International + Club Friendlies,
      Argentine Primera, Brazilian Serie A, Mexican Liga MX, MLS, NWSL,
      Chinese Super League, Colombian Primera A, Uruguayan Primera, J1 League,
      A-League, Saudi Pro League)
      Motorsport (Formula 1, NASCAR Cup/Truck, IndyCar, NHRA)
      Tennis (ATP, WTA)
      Golf (PGA, LPGA, DP World Tour)
      Fighting (UFC, PFL, Bellator)
      Baseball (MLB)
      Basketball (NBA, WNBA)
      American Football (NFL, CFL, NCAA)
      Ice Hockey (NHL)

  The panel's api/sports.php returns the URL the app loads, so you choose
  which mode by editing that endpoint.

  ------------------------------------------------------------------
  4.5  THE ONLY APK EDIT — CHANGE THE PANEL URL (the "DNS")
  ------------------------------------------------------------------
  The app finds your panel through ONE hardcoded URL baked into the APK.
  To point the app at YOUR panel, you change that one string. This is the
  only APK edit you need for a basic install.

  WHERE IT IS (FULL PATH):
    After you disassemble classes22.dex, the file is at:
        smali/ca/krankhaus/tivi/config/AppConfig.smali
    (i.e. the class  ca.krankhaus.tivi.config.AppConfig  inside classes22.dex)
   1. `smali_classes21/com/PanelURL.smali` → https://YOUR-PANEL-DOMAIN/YOUR-PANEL-FOLDER/ ✅
   2. `smali_classes22/ca/krankhaus/tivi/config/AppConfig.smali` → https://YOUR-PANEL-DOMAIN/YOUR-PANEL-FOLDER/ ✅
   3. `smali_classes22/ca/krankhaus/tivi/panel/PanelAutoImport.smali` → https://YOUR-PANEL-DOMAIN/YOUR-PANEL-FOLDER/ ✅

    Open that file and find the PANEL_BASE_URL field declaration near the
    top (around line 11):
        .field private static PANEL_BASE_URL:Ljava/lang/String; = null

    Then find the line that SETS it, inside the <clinit> static initializer
    (around line 39):
        const-string v0, "https://YOUR-PANEL-DOMAIN/YOUR-PANEL-FOLDER/"
        sput-object v0, Lca/krankhaus/tivi/config/AppConfig;->PANEL_BASE_URL:Ljava/lang/String;

  CHANGE IT TO:
        const-string v0, "https://YOUR-PANEL-DOMAIN/YOUR-PANEL-FOLDER/"
  ...where YOUR-PANEL-DOMAIN/YOUR-PANEL-FOLDER is the public URL of the
  panel folder you uploaded in step 3. KEEP the trailing slash.

  THIS IS THE ONLY URL YOU CHANGE IN THE APK. Everything else (the Discord
  page, Weather, Sports, the bot URL) is derived from this one base URL at
  runtime — the app appends "discord.php", "weather.php", "sport.php", etc.
  to it. So changing this one line repoints the whole app at your panel.

  HOW TO DECOMPILE + REBUILD (dex-swap method):
  The app has a NATIVE INTEGRITY CHECK (libcockpitdotlol.so -> isTampered)
  that crashes if protected dex files change. So you ONLY modify
  classes22.dex; classes.dex and classes2-21 must stay byte-identical.

  1. Extract classes22.dex from the APK:
       unzip -o tivimate-5-3-3-discord-voice.apk classes22.dex
  2. Disassemble it:
       baksmali d classes22.dex -o smali
  3. Edit  smali/ca/krankhaus/tivi/config/AppConfig.smali
     and change the panel URL (the const-string line above).
  4. Reassemble:
       smali assemble smali -o classes22.dex
  5. Copy the ORIGINAL APK and swap in the new dex:
       cp tivimate-5-3-3-discord-voice.apk base.apk
       zip -0 base.apk classes22.dex
  6. Align:
       zipalign -f 4 base.apk aligned.apk
  7. Sign:
       apksigner sign --ks YOUR_KEYSTORE.jks --out final.apk aligned.apk
  8. VERIFY classes.dex and classes2-21 are byte-identical to the original
     (only classes22.dex should differ). If any protected dex changed, the
     app will crash on launch.

  IMPORTANT — the cloudflare bot URL is NOT in the APK:
  The bot's public URL (the cloudflared link) is NOT baked into the APK.
  It is passed at runtime from the PANEL's config.ini ([discord] bot_url)
  to the page via the JS bridge (window.TiviBridge.setBotUrl). So:
    - The APK only needs the PANEL URL (this section).
    - The cloudflare link goes in the PANEL config.ini, NOT the APK.
  If you change the cloudflare URL later, you only edit the panel config —
  you do NOT need to rebuild the APK.

================================================================================
  PART 5 — FULL INSTALL (where to put files and what to add)
================================================================================
  ------------------------------------------------------------------
  5.1  THE PANEL (web host)
  ------------------------------------------------------------------
  1. Upload the whole panel folder to your host (e.g. into a subfolder
     like  /YOUR-PANEL-FOLDER/  under your web root).
  2. Make sure the api/ folder is writable by the web server (SQLite DB).
  3. Verify PHP 8.1+ with the SQLite3, OpenSSL and sodium extensions.
  4. Edit config.ini:
       - set your panel name / brand / contact
       - set  force_https = true  if you use SSL
       - set  [discord] bot_url  to your cloudflared URL (Part 3)
  5. Default login is  admin / admin  — CHANGE IT IMMEDIATELY.
  6. KEEP .htaccess MINIMAL (CSP only). Do NOT re-add rewrites or the
     HTTPS redirect — it 500s the whole panel on an HTTP-only host.

  ------------------------------------------------------------------
  5.2  THE BOT (server)
  ------------------------------------------------------------------
  1. Copy the discord-bot/ folder to your server.
  2. Fill in config.json (Part 1.3).
  3. Run:  bash install-and-run.sh
  4. Run:  bash install-cloudflared.sh  -> copy the URL
  5. Set that URL in: bot config.json, panel config.ini, Discord portal.
  6. (Optional) systemd auto-start (Part 1.4).

  ------------------------------------------------------------------
  5.3  THE APK (device)
  ------------------------------------------------------------------
  The working APK to install is:  tivimate-5-3-3-discord-voice.apk
  (this is the last full working build — Discord + Weather + Sports + voice)

  1. Uninstall any old TiviMate build from the device.
  2. Install  tivimate-5-3-3-discord-voice.apk
  3. Open the app. The Discord / Weather / Sports buttons appear in the
     main menu (fetched from the panel at launch).
  4. Discord: open it -> "Log in with Discord" -> Authorize -> chat room.
  5. Weather: open it -> clock + forecast appear automatically.
  6. Sports: open it -> categories | clock | listings.

  NOTE: if you change the panel URL (Part 4.5), rebuild from THIS APK
  (tivimate-5-3-3-discord-voice.apk) as the base — it is the known-good
  working build.

  ------------------------------------------------------------------
  5.4  REBUILDING THE APK (dex-swap method)
  ------------------------------------------------------------------
  If you need to rebuild the APK (e.g. to change the panel URL), use the
  dex-swap method so the integrity check passes:

  1. Start from the WORKING base APK (the one with the buttons).
  2. Extract its classes22.dex:  baksmali d classes22.dex -o smali
  3. Edit the smali (e.g. the panel URL in the AppConfig class, or the
     WebViewActivity dispatchKeyEvent for the Discord scroll/click).
  4. Reassemble:  smali assemble smali -o classes22.dex
  5. Copy the base APK, replace classes22.dex:
       cp base.apk out.apk
       zip -0 out.apk classes22.dex
  6. zipalign:  zipalign -f 4 out.apk aligned.apk
  7. Sign:  apksigner sign --ks YOUR_KEYSTORE.jks --out final.apk aligned.apk
  8. VERIFY classes.dex and classes2-21 are byte-identical to the base
     (only classes22.dex should differ). If any protected dex changed, the
     app will crash on launch.

  KEY SMALI GOTCHAS (learned the hard way):
  - In a method with .registers N and P params, the top P registers alias
    the params (p0=this, p1=first param...). Using a param register for a
    new object clobbers `this` -> CRASH on WebView open. Use only dead
    registers that are re-set before use.
  - The FireCube WebView has NO pageDown()/pageUp() methods. Always scroll
    via JavaScript (evaluateJavascript), never pageDown/pageUp.
  - Discord's oauth2 page is a MODAL DIALOG — window.scrollBy does nothing.
    Scroll the actual scrollable element under the center of the screen.
  - Gate the scroll/click on "discord.com", NOT "oauth2" (the real oauth2
    URL doesn't contain "oauth2").
  - Do NOT hijack the D-pad center (OK) — it's needed for login/navigation.
  - Do NOT add a lockdown that throws when the JS bridge is absent — it
    breaks the oauth2 login.

================================================================================
  TROUBLESHOOTING
================================================================================
  - "No Discord/Weather/Sports buttons": the app fetches the menu config
    from the panel at launch. If the panel is down, no buttons show. Also
    check the APK's classes22.dex didn't get a VerifyError.
  - "Whole panel 500": suspect .htaccess first (HTTPS redirect or a bad
    directive). Keep .htaccess minimal (CSP only).
  - "Discord bounces back to login": a lockdown that throws when the JS
    bridge is absent, or the OAuth redirect URL is wrong. Check the
    redirect URI in the Discord portal matches your cloudflared URL.
  - "Voice talks back / launches Alexa": on Fire TV the system recognizer
    routes to Alexa. Use the on-screen 🎤 key (records the device mic and
    sends to the bot's /transcribe), NOT the remote mic button.
  - "403 on buttons": file permissions on the host — set .php files to 644.
  - "Weather unavailable": the WebView can't call external APIs (CORS).
    Make sure weather.php is proxying both calls server-side.

================================================================================
  SECURITY NOTES
================================================================================
  - The bot token and OAuth client secret live ONLY in config.json on the
    server. NEVER put them in the APK or on the panel.
  - The TV only holds a short-lived session token issued after a successful
    Discord login.
  - Access to the room is gated by Discord's own membership check.
  - Change the panel's default admin password immediately.
  - If you share this folder, make sure config.json and discord-bot.service
    use placeholders, not your real values.

================================================================================
  END OF GUIDE
================================================================================
